Repository: k8sgpt-ai/k8sgpt Branch: main Commit: 2276b12b0f26 Files: 214 Total size: 1.2 MB Directory structure: gitextract_xd03t35h/ ├── .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 ├── MCP.md ├── 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-mcp-example.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 ├── go.mod ├── go.sum ├── 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 │ │ ├── groq.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 │ │ ├── catalogsource.go │ │ ├── catalogsource_test.go │ │ ├── clustercatalog.go │ │ ├── clustercatalog_test.go │ │ ├── clusterextension.go │ │ ├── clusterextension_test.go │ │ ├── clusterserviceversion.go │ │ ├── clusterserviceversion_test.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 │ │ ├── installplan_test.go │ │ ├── instalplan.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 │ │ ├── operatorgroup.go │ │ ├── operatorgroup_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 │ │ ├── subscription.go │ │ ├── subscription_test.go │ │ ├── test_utils.go │ │ ├── validating_webhook.go │ │ └── validating_webhook_test.go │ ├── cache/ │ │ ├── azuresa_based.go │ │ ├── cache.go │ │ ├── cache_test.go │ │ ├── file_based.go │ │ ├── file_based_test.go │ │ ├── gcs_based.go │ │ ├── interplex_based.go │ │ ├── interplex_based_test.go │ │ ├── s3_based.go │ │ └── types.go │ ├── common/ │ │ └── types.go │ ├── custom/ │ │ ├── client.go │ │ ├── client_test.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 │ │ ├── mcp_handlers.go │ │ ├── mcp_prompts.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 ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/CODEOWNERS ================================================ # CODEOWNERS file indicates code owners for certain files # # Code owners will automatically be added as a reviewer for PRs that touch # the owned files. # # Default owners for everything in the repo # # Unless a later match takes precedence, these owners will be requested for # review when someone opens a pull request. /.github/settings.yml @k8sgpt-ai/maintainers * @k8sgpt-ai/maintainers @k8sgpt-ai/k8sgpt-maintainers @k8sgpt-ai/k8sgpt-approvers ================================================ FILE: .github/pull_request_template.md ================================================ Closes # ## 📑 Description ## ✅ Checks - [ ] My pull request adheres to the code style of this project - [ ] My code requires changes to the documentation - [ ] I have updated the documentation as required - [ ] All the tests have passed ## ℹ Additional Information ================================================ FILE: .github/settings.yml ================================================ repository: name: "k8sgpt" description: "Giving Kubernetes SRE superpowers to everyone" homepage_url: "https://k8sgpt.ai" topics: kubernetes, devops, tooling, openai, sre default_branch: main allow_squash_merge: true allow_merge_commit: true allow_rebase_merge: true has_wiki: false teams: - name: "maintainers" permission: "admin" - name: "k8sgpt-maintainers" permission: "maintain" - name: "k8sgpt-approvers" permission: "push" - name: "contributors" permission: "push" branches: - name: main protection: required_pull_request_reviews: required_approving_review_count: 1 dismiss_stale_reviews: true require_code_owner_reviews: true dismissal_restrictions: {} code_owner_approval: true required_conversation_resolution: true required_status_checks: strict: true contexts: - "DCO" enforce_admins: true required_linear_history: true restrictions: users: [] apps: [] teams: [] ================================================ FILE: .github/workflows/build_container.yaml ================================================ name: Build container on: push: branches: - 'main' - '[0-9]+.[1-9][0-9]*.x' pull_request: branches: - 'main' - fix/build-branch - '[0-9]+.[1-9][0-9]*.x' paths-ignore: - "**.md" env: GO_VERSION: "~1.24" IMAGE_NAME: "k8sgpt" REGISTRY_IMAGE: ghcr.io/k8sgpt-ai/k8sgpt defaults: run: shell: bash jobs: prepare_ci_run: name: Prepare CI Run runs-on: ubuntu-latest outputs: GIT_SHA: ${{ steps.extract_branch.outputs.GIT_SHA }} BRANCH: ${{ steps.extract_branch.outputs.BRANCH }} BRANCH_SLUG: ${{ steps.extract_branch.outputs.BRANCH_SLUG }} DATETIME: ${{ steps.get_datetime.outputs.DATETIME }} BUILD_TIME: ${{ steps.get_datetime.outputs.BUILD_TIME }} NON_FORKED_AND_NON_ROBOT_RUN: ${{ steps.get_run_type.outputs.NON_FORKED_AND_NON_ROBOT_RUN }} steps: - name: Check out code uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 - name: Extract branch name id: extract_branch uses: keptn/gh-action-extract-branch-name@main - name: Get current date and time id: get_datetime run: | DATETIME=$(date +'%Y%m%d%H%M') BUILD_TIME=$(date -u "+%F_%T") echo "DATETIME=$DATETIME" >> "$GITHUB_OUTPUT" echo "BUILD_TIME=$BUILD_TIME" >> "$GITHUB_OUTPUT" - name: Get workflow run type id: get_run_type run: | NON_FORKED_AND_NON_ROBOT_RUN=${{ ( github.actor != 'renovate[bot]' && github.actor != 'dependabot[bot]' ) && ( github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository ) }} echo "NON_FORKED_AND_NON_ROBOT_RUN=$NON_FORKED_AND_NON_ROBOT_RUN" >> "$GITHUB_OUTPUT" build-and-push: name: Build and Push Multi-arch Image needs: prepare_ci_run runs-on: ubuntu-latest if: ${{ needs.prepare_ci_run.outputs.NON_FORKED_AND_NON_ROBOT_RUN == 'true' }} env: DATETIME: ${{ needs.prepare_ci_run.outputs.DATETIME }} BUILD_TIME: ${{ needs.prepare_ci_run.outputs.BUILD_TIME }} GIT_SHA: ${{ needs.prepare_ci_run.outputs.GIT_SHA }} steps: - name: Check out code uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 - name: Docker meta id: meta uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5 with: images: ${{ env.REGISTRY_IMAGE }} tags: | type=ref,event=branch type=ref,event=pr type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=raw,value=dev-${{ env.DATETIME }} - name: Login to GitHub Container Registry uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.K8SGPT_BOT_SECRET }} - name: Set up QEMU uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3 - name: Build and push multi-arch image uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6 with: context: . file: ./container/Dockerfile platforms: linux/amd64,linux/arm64 push: true target: production build-args: | GIT_HASH=${{ env.GIT_SHA }} RELEASE_VERSION=dev-${{ env.DATETIME }} BUILD_TIME=${{ env.BUILD_TIME }} tags: | ${{ env.REGISTRY_IMAGE }}:${{ env.DATETIME }} labels: ${{ steps.meta.outputs.labels }} secrets: | GIT_AUTH_TOKEN=${{ secrets.K8SGPT_BOT_SECRET }} ================================================ FILE: .github/workflows/golangci_lint.yaml ================================================ name: Run golangci-lint on: pull_request: branches: [main] jobs: golangci-lint: runs-on: ubuntu-latest steps: - name: Check out code into the Go module directory uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 - name: golangci-lint uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8 with: version: v2.1.0 only-new-issues: true ================================================ FILE: .github/workflows/release.yaml ================================================ name: release on: push: branches: - main - '[0-9]+.[0-9]+.x' workflow_dispatch: defaults: run: shell: bash jobs: release-please: permissions: contents: write # for google-github-actions/release-please-action to create release commit pull-requests: write # for google-github-actions/release-please-action to create release PR runs-on: ubuntu-latest outputs: releases_created: ${{ steps.release.outputs.releases_created }} tag_name: ${{ steps.release.outputs.tag_name }} # Release-please creates a PR that tracks all changes steps: - name: Checkout uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 - uses: google-github-actions/release-please-action@e4dc86ba9405554aeba3c6bb2d169500e7d3b4ee # v4.1.1 id: release with: command: manifest token: ${{secrets.GITHUB_TOKEN}} default-branch: main goreleaser: if: needs.release-please.outputs.releases_created == 'true' permissions: contents: write needs: - release-please runs-on: ubuntu-latest steps: - name: Free Disk Space (Ubuntu) uses: jlumbroso/free-disk-space@main with: # this might remove tools that are actually needed, # if set to "true" but frees about 6 GB tool-cache: false # all of these default to true, but feel free to set to # "false" if necessary for your workflow android: false dotnet: false haskell: false large-packages: true docker-images: true swap-storage: true - name: Checkout uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 with: fetch-depth: 0 - name: Set up Go uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 with: go-version: '~1.24' - name: Download Syft uses: anchore/sbom-action/download-syft@55dc4ee22412511ee8c3142cbea40418e6cec693 # v0.17.8 - name: Run GoReleaser uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6 with: # either 'goreleaser' (default) or 'goreleaser-pro' distribution: goreleaser version: latest args: release --clean env: GITHUB_TOKEN: ${{ secrets.K8SGPT_BOT_SECRET }} SLACK_TOKEN: ${{ secrets.SLACK_TOKEN }} # - name: Update new version in krew-index # uses: rajatjindal/krew-release-bot@3d9faef30a82761d610544f62afddca00993eef9 # v0.0.47 build-container: if: needs.release-please.outputs.releases_created == 'true' needs: - release-please runs-on: ubuntu-latest permissions: contents: write packages: write id-token: write env: IMAGE_TAG: ghcr.io/k8sgpt-ai/k8sgpt:${{ needs.release-please.outputs.tag_name }} IMAGE_NAME: k8sgpt steps: - name: Checkout uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 with: submodules: recursive - name: Set up Docker Buildx id: buildx uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3 - name: Login to GitHub Container Registry uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3 with: registry: "ghcr.io" username: ${{ github.actor }} password: ${{ secrets.K8SGPT_BOT_SECRET }} - name: Build Docker Image uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6 with: context: . file: ./container/Dockerfile platforms: linux/amd64,linux/arm64 target: production tags: | ${{ env.IMAGE_TAG }} builder: ${{ steps.buildx.outputs.name }} push: true cache-from: type=gha,scope=${{ github.ref_name }}-${{ env.IMAGE_TAG }} cache-to: type=gha,scope=${{ github.ref_name }}-${{ env.IMAGE_TAG }} - name: Generate SBOM uses: anchore/sbom-action@55dc4ee22412511ee8c3142cbea40418e6cec693 # v0.17.8 with: image: ${{ env.IMAGE_TAG }} artifact-name: sbom-${{ env.IMAGE_NAME }} output-file: ./sbom-${{ env.IMAGE_NAME }}.spdx.json - name: Attach SBOM to release uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2 with: tag_name: ${{ needs.release-please.outputs.tag_name }} files: ./sbom-${{ env.IMAGE_NAME }}.spdx.json ================================================ FILE: .github/workflows/semantic_pr.yaml ================================================ name: Semantic PR Validation on: pull_request_target: types: - opened - edited - synchronize defaults: run: shell: bash jobs: validate: runs-on: ubuntu-latest permissions: contents: read # Needed for checking out the repository pull-requests: read # Needed for reading prs steps: - name: Validate Pull Request uses: amannn/action-semantic-pull-request@fdd4d3ddf614fbcd8c29e4b106d3bbe0cb2c605d # v6.0.1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: # Configure which types are allowed. # Default: https://github.com/commitizen/conventional-commit-types types: | feat fix build chore ci docs perf refactor revert style test deps scopes: | deps # Configure that a scope must always be provided. requireScope: false # When using "Squash and merge" on a PR with only one commit, GitHub # will suggest using that commit message instead of the PR title for the # merge commit, and it's easy to commit this by mistake. Enable this option # to also validate the commit message for one commit PRs. validateSingleCommit: true # Configure additional validation for the subject based on a regex. # This ensures the subject doesn't start with an uppercase character. subjectPattern: ^(?![A-Z]).+$ # If `subjectPattern` is configured, you can use this property to override # the default error message that is shown when the pattern doesn't match. # The variables `subject` and `title` can be used within the message. subjectPatternError: | The subject "{subject}" found in the pull request title "{title}" didn't match the configured pattern. Please ensure that the subject doesn't start with an uppercase character. ================================================ FILE: .github/workflows/test.yaml ================================================ name: Run tests on: push: branches: - main pull_request: branches: - main env: GO_VERSION: "~1.24" jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 - name: Set up Go uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 with: go-version: ${{ env.GO_VERSION }} - name: Run test run: go test ./... -coverprofile=coverage.txt - name: Upload coverage to Codecov uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} ================================================ FILE: .gitignore ================================================ .idea __debug* .DS_Store k8sgpt* !charts/k8sgpt *.vscode dist/ bin/ pkg/server/example/example ================================================ FILE: .goreleaser.yaml ================================================ version: 2 # This is an example .goreleaser.yml file with some sensible defaults. # Make sure to check the documentation at https://goreleaser.com before: hooks: # You may remove this if you don't use go modules. - go mod tidy # you may remove this if you don't need go generate - go generate ./... builds: - env: - CGO_ENABLED=0 goos: - linux - windows - darwin ldflags: - -s -w - -X main.version={{.Version}} - -X main.commit={{.ShortCommit}} - -X main.Date={{.CommitDate}} nfpms: - file_name_template: "{{ .ProjectName }}_{{ .Arch }}" maintainer: "K8sGPT Maintainers " homepage: https://k8sgpt.ai description: >- 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. license: "Apache-2.0" formats: - deb - rpm - apk bindir: /usr/bin section: utils contents: - src: ./LICENSE dst: /usr/share/doc/k8sgpt/copyright file_info: mode: 0644 sboms: - artifacts: archive archives: - format: tar.gz # this name template makes the OS and Arch compatible with the results of uname. name_template: >- {{ .ProjectName }}_ {{- title .Os }}_ {{- if eq .Arch "amd64" }}x86_64 {{- else if eq .Arch "386" }}i386 {{- else }}{{ .Arch }}{{ end }} {{- if .Arm }}v{{ .Arm }}{{ end }} # use zip for windows archives format_overrides: - goos: windows format: zip brews: - name: k8sgpt homepage: https://k8sgpt.ai repository: owner: k8sgpt-ai name: homebrew-k8sgpt checksum: name_template: "checksums.txt" snapshot: name_template: "{{ incpatch .Version }}-next" announce: slack: # Whether its enabled or not. # # Templates: allowed (since v2.6). enabled: true # Message template to use while publishing. # # Default: '{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}'. # Templates: allowed. message_template: "{{ .ProjectName }} release {{.Tag}} is out!" # The name of the channel that the user selected as a destination for webhook messages. channel: "#general" # Set your Webhook's user name. username: "K8sGPT" # Emoji to use as the icon for this message. Overrides icon_url. icon_emoji: "" # URL to an image to use as the icon for this message. icon_url: "" ================================================ FILE: .krew.yaml ================================================ apiVersion: krew.googlecontainertools.github.com/v1alpha2 kind: Plugin metadata: name: gpt spec: version: {{ .TagName }} homepage: https://github.com/k8sgpt-ai/k8sgpt shortDescription: "Giving Kubernetes Superpowers to everyone" description: | A tool for scanning your Kubernetes clusters, diagnosing, and triaging issues in simple English. platforms: ########## # Darwin # ########## - selector: matchLabels: os: darwin arch: amd64 {{addURIAndSha "https://github.com/k8sgpt-ai/k8sgpt/releases/download/{{ .TagName }}/k8sgpt_Darwin_x86_64.tar.gz" .TagName | indent 6 }} files: - from: "k8sgpt" to: "kubectl-gpt" - from: "LICENSE" to: "." bin: kubectl-gpt - selector: matchLabels: os: darwin arch: arm64 {{addURIAndSha "https://github.com/k8sgpt-ai/k8sgpt/releases/download/{{ .TagName }}/k8sgpt_Darwin_arm64.tar.gz" .TagName | indent 6 }} files: - from: "k8sgpt" to: "kubectl-gpt" - from: "LICENSE" to: "." bin: kubectl-gpt ######### # Linux # ######### - selector: matchLabels: os: linux arch: amd64 {{addURIAndSha "https://github.com/k8sgpt-ai/k8sgpt/releases/download/{{ .TagName }}/k8sgpt_Linux_x86_64.tar.gz" .TagName | indent 6 }} files: - from: "k8sgpt" to: "kubectl-gpt" - from: "LICENSE" to: "." bin: kubectl-gpt - selector: matchLabels: os: linux arch: arm64 {{addURIAndSha "https://github.com/k8sgpt-ai/k8sgpt/releases/download/{{ .TagName }}/k8sgpt_Linux_arm64.tar.gz" .TagName | indent 6 }} files: - from: "k8sgpt" to: "kubectl-gpt" - from: "LICENSE" to: "." bin: kubectl-gpt - selector: matchLabels: os: linux arch: "386" {{addURIAndSha "https://github.com/k8sgpt-ai/k8sgpt/releases/download/{{ .TagName }}/k8sgpt_Linux_i386.tar.gz" .TagName | indent 6 }} files: - from: "k8sgpt" to: "kubectl-gpt" - from: "LICENSE" to: "." bin: kubectl-gpt ########### # Windows # ########### - selector: matchLabels: os: windows arch: amd64 {{addURIAndSha "https://github.com/k8sgpt-ai/k8sgpt/releases/download/{{ .TagName }}/k8sgpt_Windows_x86_64.zip" .TagName | indent 6 }} files: - from: "k8sgpt" to: "kubectl-gpt" - from: "LICENSE" to: "." bin: kubectl-gpt - selector: matchLabels: os: windows arch: arm64 {{addURIAndSha "https://github.com/k8sgpt-ai/k8sgpt/releases/download/{{ .TagName }}/k8sgpt_Windows_arm64.zip" .TagName | indent 6 }} files: - from: "k8sgpt" to: "kubectl-gpt" - from: "LICENSE" to: "." bin: kubectl-gpt - selector: matchLabels: os: windows arch: "386" {{addURIAndSha "https://github.com/k8sgpt-ai/k8sgpt/releases/download/{{ .TagName }}/k8sgpt_Windows_i386.zip" .TagName | indent 6 }} files: - from: "k8sgpt" to: "kubectl-gpt" - from: "LICENSE" to: "." bin: kubectl-gpt ================================================ FILE: .release-please-manifest.json ================================================ {".":"0.4.30"} ================================================ FILE: CHANGELOG.md ================================================ # Changelog ## [0.4.30](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.29...v0.4.30) (2026-02-20) ### Bug Fixes * validate namespace before running custom analyzers ([#1617](https://github.com/k8sgpt-ai/k8sgpt/issues/1617)) ([458aa9d](https://github.com/k8sgpt-ai/k8sgpt/commit/458aa9debac7590eb0855ffd12141b702e999a36)) ## [0.4.29](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.28...v0.4.29) (2026-02-20) ### Features * **serve:** add short flag and env var for metrics port ([#1616](https://github.com/k8sgpt-ai/k8sgpt/issues/1616)) ([4f63e97](https://github.com/k8sgpt-ai/k8sgpt/commit/4f63e9737c6a2306686bd3b6f37e81f210665949)) ### Bug Fixes * **deps:** update k8s.io/utils digest to b8788ab ([#1572](https://github.com/k8sgpt-ai/k8sgpt/issues/1572)) ([a56e478](https://github.com/k8sgpt-ai/k8sgpt/commit/a56e4788c3361a64df17175f163f33422a8fe606)) * use proper JSON marshaling for customrest prompt to handle special characters ([#1615](https://github.com/k8sgpt-ai/k8sgpt/issues/1615)) ([99911fb](https://github.com/k8sgpt-ai/k8sgpt/commit/99911fbb3ac8c950fd7ee1b3210f8a9c2a6b0ad7)), closes [#1556](https://github.com/k8sgpt-ai/k8sgpt/issues/1556) ### Refactoring * improve MCP server handlers with better error handling and pagination ([#1613](https://github.com/k8sgpt-ai/k8sgpt/issues/1613)) ([abc4647](https://github.com/k8sgpt-ai/k8sgpt/commit/abc46474e372bcd27201f1a64372c04269acee13)) ## [0.4.28](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.27...v0.4.28) (2026-02-15) ### Features * add Groq as LLM provider ([#1600](https://github.com/k8sgpt-ai/k8sgpt/issues/1600)) ([867bce1](https://github.com/k8sgpt-ai/k8sgpt/commit/867bce1907f5dd3387128b72c694e98091d55554)) * multiple security fixes. Prometheus: v0.302.1 → v0.306.0 ([#1597](https://github.com/k8sgpt-ai/k8sgpt/issues/1597)) ([f5fb2a7](https://github.com/k8sgpt-ai/k8sgpt/commit/f5fb2a7e12e14fad8107940aeead5e60b064add1)) ### Bug Fixes * align CI Go versions with go.mod to ensure consistency ([#1611](https://github.com/k8sgpt-ai/k8sgpt/issues/1611)) ([1f2ff98](https://github.com/k8sgpt-ai/k8sgpt/commit/1f2ff988342b8ef2aa3e3263eb845c0ee09fe24c)) * **deps:** update module gopkg.in/yaml.v2 to v3 ([#1550](https://github.com/k8sgpt-ai/k8sgpt/issues/1550)) ([7fe3bdb](https://github.com/k8sgpt-ai/k8sgpt/commit/7fe3bdbd952bc9a1975121de5f21ad31dc1f691d)) * use MaxCompletionTokens instead of deprecated MaxTokens for OpenAI ([#1604](https://github.com/k8sgpt-ai/k8sgpt/issues/1604)) ([c80b2e2](https://github.com/k8sgpt-ai/k8sgpt/commit/c80b2e2c346845336593ce515fe90fd501b1d0a7)) ### Other * **deps:** update actions/checkout digest to 93cb6ef ([#1592](https://github.com/k8sgpt-ai/k8sgpt/issues/1592)) ([40ffcbe](https://github.com/k8sgpt-ai/k8sgpt/commit/40ffcbec6b65e3a99e40be5f414a3f2c087bffbb)) * **deps:** update actions/setup-go digest to 40f1582 ([#1593](https://github.com/k8sgpt-ai/k8sgpt/issues/1593)) ([a303ffa](https://github.com/k8sgpt-ai/k8sgpt/commit/a303ffa21c7ede3dd9391185bc91fb3b4e8276b6)) * util tests ([#1594](https://github.com/k8sgpt-ai/k8sgpt/issues/1594)) ([21369c5](https://github.com/k8sgpt-ai/k8sgpt/commit/21369c5c0917fd2b6ae4173378b2e257e2b1de7b)) ## [0.4.27](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.26...v0.4.27) (2025-12-18) ### Features * mcp v2 ([#1589](https://github.com/k8sgpt-ai/k8sgpt/issues/1589)) ([5480051](https://github.com/k8sgpt-ai/k8sgpt/commit/5480051230ce83b89c0382abd7992c7ecc4a85b8)) ## [0.4.26](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.25...v0.4.26) (2025-10-16) ### Other * missing filter arg on serve ([#1583](https://github.com/k8sgpt-ai/k8sgpt/issues/1583)) ([f1d2e30](https://github.com/k8sgpt-ai/k8sgpt/commit/f1d2e306f32eb1e01a2788174084be29a7fa1282)) ## [0.4.25](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.24...v0.4.25) (2025-09-03) ### Features * fix to broken inference ([#1575](https://github.com/k8sgpt-ai/k8sgpt/issues/1575)) ([291e42d](https://github.com/k8sgpt-ai/k8sgpt/commit/291e42dc4b81ffb0672c21fbb325ddebc5d531a3)) ## [0.4.24](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.23...v0.4.24) (2025-08-18) ### Features * add ClusterServiceVersion, Subscription, InstallPlan, OperatorGroup, and CatalogSource analyzers ([#1564](https://github.com/k8sgpt-ai/k8sgpt/issues/1564)) ([0cf4cae](https://github.com/k8sgpt-ai/k8sgpt/commit/0cf4cae07e32a0025246abcf2d1a5a91f82d093a)) * reintroduced inference code ([#1548](https://github.com/k8sgpt-ai/k8sgpt/issues/1548)) ([7e33276](https://github.com/k8sgpt-ai/k8sgpt/commit/7e332761d89d953989b4f33509208dd4db4d4b91)) * update helm charts with mcp support and fix Google ADA issue ([#1568](https://github.com/k8sgpt-ai/k8sgpt/issues/1568)) ([5334589](https://github.com/k8sgpt-ai/k8sgpt/commit/53345895deec4c74cac00ee3fd5e230f6a92cf4a)) ### Bug Fixes * migrated to more actively maintained mcp golang lib and added AI explain ([#1557](https://github.com/k8sgpt-ai/k8sgpt/issues/1557)) ([c47ae59](https://github.com/k8sgpt-ai/k8sgpt/commit/c47ae595fb9fc5bf22afef3bc6764b3e87e4553d)) ### Other * **deps:** update actions/checkout action to v5 ([#1562](https://github.com/k8sgpt-ai/k8sgpt/issues/1562)) ([e385e77](https://github.com/k8sgpt-ai/k8sgpt/commit/e385e77da93a65fe52a152bf1f8f1415552698d5)) * **deps:** update amannn/action-semantic-pull-request action to v6 ([#1565](https://github.com/k8sgpt-ai/k8sgpt/issues/1565)) ([c5c9135](https://github.com/k8sgpt-ai/k8sgpt/commit/c5c9135900ec6f95b63dac47df751269e7420e87)) * **deps:** update docker/login-action digest to 184bdaa ([#1559](https://github.com/k8sgpt-ai/k8sgpt/issues/1559)) ([0239b2f](https://github.com/k8sgpt-ai/k8sgpt/commit/0239b2fe6e7105bbcf3256c559c30ec7065b25f3)) * **deps:** update goreleaser/goreleaser-action digest to e435ccd ([#1569](https://github.com/k8sgpt-ai/k8sgpt/issues/1569)) ([5e86f49](https://github.com/k8sgpt-ai/k8sgpt/commit/5e86f4925c4209b0eb2959227229c2994cfc5b6f)) ## [0.4.23](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.22...v0.4.23) (2025-08-08) ### Features * add ClusterCatalog and ClusterExtension analyzers ([#1555](https://github.com/k8sgpt-ai/k8sgpt/issues/1555)) ([a821814](https://github.com/k8sgpt-ai/k8sgpt/commit/a821814125e25c062ff2faebf9df1b880414c22c)) * oci genai chat models ([#1337](https://github.com/k8sgpt-ai/k8sgpt/issues/1337)) ([290a4be](https://github.com/k8sgpt-ai/k8sgpt/commit/290a4be210fbb508214070c31218138781d96142)) ### Bug Fixes * **deps:** update module gopkg.in/yaml.v2 to v3 ([#1537](https://github.com/k8sgpt-ai/k8sgpt/issues/1537)) ([50d5d78](https://github.com/k8sgpt-ai/k8sgpt/commit/50d5d78c06e42d75a2448989528e5e6be12ea825)) * **deps:** update module helm.sh/helm/v3 to v3.17.4 [security] ([#1541](https://github.com/k8sgpt-ai/k8sgpt/issues/1541)) ([5b42249](https://github.com/k8sgpt-ai/k8sgpt/commit/5b4224951e7348e9d78292dadc9b9786957117f1)) ## [0.4.22](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.21...v0.4.22) (2025-07-18) ### Features * add APAC region Claude models support for Amazon Bedrock ([#1543](https://github.com/k8sgpt-ai/k8sgpt/issues/1543)) ([1819e6f](https://github.com/k8sgpt-ai/k8sgpt/commit/1819e6f410d078fce2bda8bbdb22054dfb4fc092)) * add streamable-http support for MCP server ([#1546](https://github.com/k8sgpt-ai/k8sgpt/issues/1546)) ([3a1187a](https://github.com/k8sgpt-ai/k8sgpt/commit/3a1187ad5a190713b9216cf6d9d52d54cdb3e4da)) ## [0.4.21](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.20...v0.4.21) (2025-06-27) ### Features * add latest and legacy stable models ([#1539](https://github.com/k8sgpt-ai/k8sgpt/issues/1539)) ([00c0799](https://github.com/k8sgpt-ai/k8sgpt/commit/00c07999e2290e70a6ecb95b255b4924f55ecd5f)) * support for claude4 && model names listed ([#1540](https://github.com/k8sgpt-ai/k8sgpt/issues/1540)) ([8002d94](https://github.com/k8sgpt-ai/k8sgpt/commit/8002d943453aac8c3675d7072b25dfdc3aec1c1d)) ### Bug Fixes * **deps:** update module gopkg.in/yaml.v2 to v3 ([#1511](https://github.com/k8sgpt-ai/k8sgpt/issues/1511)) ([08f2855](https://github.com/k8sgpt-ai/k8sgpt/commit/08f2855a4d7e61f3422cb68b0966272a85f617a5)) ### Other * **deps:** update docker/setup-buildx-action digest to e468171 ([#1527](https://github.com/k8sgpt-ai/k8sgpt/issues/1527)) ([0c917fc](https://github.com/k8sgpt-ai/k8sgpt/commit/0c917fc60115ef0dc775e858a55964382b20c5e1)) ## [0.4.20](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.19...v0.4.20) (2025-06-20) ### Features * added cache purge ([#1532](https://github.com/k8sgpt-ai/k8sgpt/issues/1532)) ([74fbde0](https://github.com/k8sgpt-ai/k8sgpt/commit/74fbde00537e627c408b317ff9098227be11e2ad)) ### Other * model name ([#1535](https://github.com/k8sgpt-ai/k8sgpt/issues/1535)) ([0f700f0](https://github.com/k8sgpt-ai/k8sgpt/commit/0f700f0cd39bf5881d6c05240b842f4df7a6c016)) ## [0.4.19](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.18...v0.4.19) (2025-06-20) ### Features * fixed haiku ([#1530](https://github.com/k8sgpt-ai/k8sgpt/issues/1530)) ([5636515](https://github.com/k8sgpt-ai/k8sgpt/commit/5636515db98b529689a214af5066d50b5e42d3a1)) ## [0.4.18](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.17...v0.4.18) (2025-06-20) ### Bug Fixes * **deps:** update k8s.io/utils digest to 4c0f3b2 ([#1523](https://github.com/k8sgpt-ai/k8sgpt/issues/1523)) ([7d4cb26](https://github.com/k8sgpt-ai/k8sgpt/commit/7d4cb267130f60088350213482795f37594cb0bc)) * **deps:** update module gopkg.in/yaml.v2 to v3 ([#1509](https://github.com/k8sgpt-ai/k8sgpt/issues/1509)) ([d7cb19a](https://github.com/k8sgpt-ai/k8sgpt/commit/d7cb19ad29c92eaba552ba723945c937fc3c42da)) ### Other * **deps:** update codecov/codecov-action digest to 18283e0 ([#1513](https://github.com/k8sgpt-ai/k8sgpt/issues/1513)) ([42654e7](https://github.com/k8sgpt-ai/k8sgpt/commit/42654e7f55d7a9e9be5b664adaaa8979106e7298)) * **deps:** update docker/build-push-action digest to 1dc7386 ([#1512](https://github.com/k8sgpt-ai/k8sgpt/issues/1512)) ([dfcc5dc](https://github.com/k8sgpt-ai/k8sgpt/commit/dfcc5dc5a15a3d59a7f6317944784e3ecd86fb50)) * **deps:** update docker/build-push-action digest to 2634353 ([#1517](https://github.com/k8sgpt-ai/k8sgpt/issues/1517)) ([7dfe8be](https://github.com/k8sgpt-ai/k8sgpt/commit/7dfe8bef0face65f607475a6620923fdfed57961)) * **deps:** update softprops/action-gh-release digest to 72f2c25 ([#1526](https://github.com/k8sgpt-ai/k8sgpt/issues/1526)) ([5947876](https://github.com/k8sgpt-ai/k8sgpt/commit/5947876e4942729eea883937faf5e2b47d1f16ec)) * **deps:** update softprops/action-gh-release digest to d5382d3 ([#1525](https://github.com/k8sgpt-ai/k8sgpt/issues/1525)) ([6b9f346](https://github.com/k8sgpt-ai/k8sgpt/commit/6b9f346bf668ed3517b23b99000611ea14afafe2)) * model access ([#1529](https://github.com/k8sgpt-ai/k8sgpt/issues/1529)) ([be4fb1c](https://github.com/k8sgpt-ai/k8sgpt/commit/be4fb1cc034d9c3843cf3e9912a26e05bd54c146)) ## [0.4.17](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.16...v0.4.17) (2025-05-14) ### Features * adding fixes for Messages API issue 1391 ([#1504](https://github.com/k8sgpt-ai/k8sgpt/issues/1504)) ([b2241c0](https://github.com/k8sgpt-ai/k8sgpt/commit/b2241c03c975aeab02897d73e57cd351f60f3af3)) * new job analyzer ([#1506](https://github.com/k8sgpt-ai/k8sgpt/issues/1506)) ([0b7ddf5](https://github.com/k8sgpt-ai/k8sgpt/commit/0b7ddf5e3b93e56ea92dfb6447e97c067cad9e54)) ### Bug Fixes * align documentation to reflect default analyzers properly ([#1498](https://github.com/k8sgpt-ai/k8sgpt/issues/1498)) ([7e375a3](https://github.com/k8sgpt-ai/k8sgpt/commit/7e375a30bee24198f9221e4a4aea17fcd2fe005c)) * **deps:** update module gopkg.in/yaml.v2 to v3 ([#1454](https://github.com/k8sgpt-ai/k8sgpt/issues/1454)) ([d0f0364](https://github.com/k8sgpt-ai/k8sgpt/commit/d0f03641ae372a00cd0eca1f41ef30a988d436bc)) * **deps:** update module gopkg.in/yaml.v2 to v3 ([#1500](https://github.com/k8sgpt-ai/k8sgpt/issues/1500)) ([d308c51](https://github.com/k8sgpt-ai/k8sgpt/commit/d308c511fbe06e012c641dfa08c4dcf4181b243a)) * panic in k8sgpt auth update ([#1497](https://github.com/k8sgpt-ai/k8sgpt/issues/1497)) ([cae94e7](https://github.com/k8sgpt-ai/k8sgpt/commit/cae94e7b6df1684a3b61af3e7aa0f4e68e8df594)) ### Other * **deps:** update actions/setup-go digest to d35c59a ([#1495](https://github.com/k8sgpt-ai/k8sgpt/issues/1495)) ([e76bdb0](https://github.com/k8sgpt-ai/k8sgpt/commit/e76bdb0c23b7d23972d99661c8fe1bffe5f9f398)) * **deps:** update golangci/golangci-lint-action action to v8 ([#1490](https://github.com/k8sgpt-ai/k8sgpt/issues/1490)) ([1e57b77](https://github.com/k8sgpt-ai/k8sgpt/commit/1e57b7774c20bda4ae0b0d765278bcd3504cfb33)) * golangci lint ([#1508](https://github.com/k8sgpt-ai/k8sgpt/issues/1508)) ([4faf77d](https://github.com/k8sgpt-ai/k8sgpt/commit/4faf77d91a3da8fdd6166ec1c381a151e5846057)) ## [0.4.16](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.15...v0.4.16) (2025-05-06) ### Features * add support for Amazon Bedrock Inference Profiles ([#1492](https://github.com/k8sgpt-ai/k8sgpt/issues/1492)) ([21bc76e](https://github.com/k8sgpt-ai/k8sgpt/commit/21bc76e5b77524b48f09ef6707204742dcd879a7)) * enhancement of deployment analyzer ([#1406](https://github.com/k8sgpt-ai/k8sgpt/issues/1406)) ([61b60d5](https://github.com/k8sgpt-ai/k8sgpt/commit/61b60d5768b54f98232dcc415e89aa38987dc6e3)) * supported regions govcloud ([#1483](https://github.com/k8sgpt-ai/k8sgpt/issues/1483)) ([752a16c](https://github.com/k8sgpt-ai/k8sgpt/commit/752a16c40728f42f10ab6c3177cb7e24f44db339)) ### Bug Fixes * **deps:** update k8s.io/utils digest to 0f33e8f ([#1484](https://github.com/k8sgpt-ai/k8sgpt/issues/1484)) ([6a81d2c](https://github.com/k8sgpt-ai/k8sgpt/commit/6a81d2c140f00a405b651d6c6dae5e343ffddb4f)) ### Other * **deps:** update docker/build-push-action digest to 14487ce ([#1472](https://github.com/k8sgpt-ai/k8sgpt/issues/1472)) ([81da402](https://github.com/k8sgpt-ai/k8sgpt/commit/81da402d46e1a1db83a41b717dfb23eb07d2e919)) * **deps:** update golangci/golangci-lint-action digest to 9fae48a ([#1489](https://github.com/k8sgpt-ai/k8sgpt/issues/1489)) ([d5341f3](https://github.com/k8sgpt-ai/k8sgpt/commit/d5341f3c0019c1114254ac05f00c743a0354ec0b)) ## [0.4.15](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.14...v0.4.15) (2025-04-29) ### Features * added token for goreleaser ([#1476](https://github.com/k8sgpt-ai/k8sgpt/issues/1476)) ([85935a4](https://github.com/k8sgpt-ai/k8sgpt/commit/85935a46d8f137b0339435cf19ce7f83ead97f8c)) ## [0.4.14](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.13...v0.4.14) (2025-04-29) ### Features * add MCP support ([#1471](https://github.com/k8sgpt-ai/k8sgpt/issues/1471)) ([e41ffd8](https://github.com/k8sgpt-ai/k8sgpt/commit/e41ffd80d01ce7ae1fac9ce7e07344020d8bf914)) * using modelName will calling completion ([#1469](https://github.com/k8sgpt-ai/k8sgpt/issues/1469)) ([f603948](https://github.com/k8sgpt-ai/k8sgpt/commit/f603948935f1c4cb171378634714577205de7b08)) ## [0.4.13](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.12...v0.4.13) (2025-04-22) ### Features * slack announce ([#1466](https://github.com/k8sgpt-ai/k8sgpt/issues/1466)) ([3b6ad06](https://github.com/k8sgpt-ai/k8sgpt/commit/3b6ad06de1121c870fb486e0fe2bd1f87be16627)) ### Bug Fixes * reverse hpa ScalingLimited error condition ([#1366](https://github.com/k8sgpt-ai/k8sgpt/issues/1366)) ([ebb0373](https://github.com/k8sgpt-ai/k8sgpt/commit/ebb0373f69ad64a6cc43d0695d07e1d076c6366e)) ### Other * **deps:** update softprops/action-gh-release digest to da05d55 ([#1464](https://github.com/k8sgpt-ai/k8sgpt/issues/1464)) ([4434699](https://github.com/k8sgpt-ai/k8sgpt/commit/443469960a6b6791e358ee0a97e4c1dc5c3018e6)) ## [0.4.12](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.11...v0.4.12) (2025-04-17) ### Features * new analyzers ([#1459](https://github.com/k8sgpt-ai/k8sgpt/issues/1459)) ([a128906](https://github.com/k8sgpt-ai/k8sgpt/commit/a128906136431189812d4d2dea68ea98cbfe5eeb)) ### Bug Fixes * **deps:** update module golang.org/x/net to v0.38.0 [security] ([#1462](https://github.com/k8sgpt-ai/k8sgpt/issues/1462)) ([e588fc3](https://github.com/k8sgpt-ai/k8sgpt/commit/e588fc316d29a29a7dde6abe2302833b38f1d302)) ### Other * **deps:** update codecov/codecov-action digest to ad3126e ([#1456](https://github.com/k8sgpt-ai/k8sgpt/issues/1456)) ([0553b98](https://github.com/k8sgpt-ai/k8sgpt/commit/0553b984b7c87b345f171bf6e5d632d890db689c)) ## [0.4.11](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.10...v0.4.11) (2025-04-15) ### Features * add verbose flag to enable detailed output ([#1420](https://github.com/k8sgpt-ai/k8sgpt/issues/1420)) ([a79224e](https://github.com/k8sgpt-ai/k8sgpt/commit/a79224e2bf96f458dbc96404c8f4847970e8d2ef)) * call bedrock with inference profile ([#1449](https://github.com/k8sgpt-ai/k8sgpt/issues/1449)) ([91d423b](https://github.com/k8sgpt-ai/k8sgpt/commit/91d423b147ca18cda7d54ff19349938a894ecb85)) * improved test coverage ([#1455](https://github.com/k8sgpt-ai/k8sgpt/issues/1455)) ([80904e3](https://github.com/k8sgpt-ai/k8sgpt/commit/80904e3063b00b0536171b7b62b938938b20825a)) ### Bug Fixes * config ai provider in query ([#1457](https://github.com/k8sgpt-ai/k8sgpt/issues/1457)) ([df17e3e](https://github.com/k8sgpt-ai/k8sgpt/commit/df17e3e728591e974703527dff86de882af17790)) * **deps:** update module gopkg.in/yaml.v2 to v3 ([#1447](https://github.com/k8sgpt-ai/k8sgpt/issues/1447)) ([969fe99](https://github.com/k8sgpt-ai/k8sgpt/commit/969fe99b3320c313f1c97133cdffb668a00d5fb5)) * **deps:** update module gopkg.in/yaml.v2 to v3 ([#1453](https://github.com/k8sgpt-ai/k8sgpt/issues/1453)) ([cf6f928](https://github.com/k8sgpt-ai/k8sgpt/commit/cf6f9289e13ee729c24968fd771c901f412e8db7)) ### Docs * fix the slack invite link ([#1450](https://github.com/k8sgpt-ai/k8sgpt/issues/1450)) ([9ce3346](https://github.com/k8sgpt-ai/k8sgpt/commit/9ce33469d85aa0829e995e4b404ae85734124fb4)) ## [0.4.10](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.9...v0.4.10) (2025-04-10) ### Features * add a naive support of bedrock inference profile ([#1446](https://github.com/k8sgpt-ai/k8sgpt/issues/1446)) ([78ffa59](https://github.com/k8sgpt-ai/k8sgpt/commit/78ffa5904addf71caf04554966437b14351f21e5)) ### Bug Fixes * **deps:** update module gopkg.in/yaml.v2 to v3 ([#1417](https://github.com/k8sgpt-ai/k8sgpt/issues/1417)) ([ce4b3c2](https://github.com/k8sgpt-ai/k8sgpt/commit/ce4b3c2e7d0762093506d9010eceb47a2dcdf5bc)) * **deps:** update module helm.sh/helm/v3 to v3.17.3 [security] ([#1448](https://github.com/k8sgpt-ai/k8sgpt/issues/1448)) ([060a3b2](https://github.com/k8sgpt-ai/k8sgpt/commit/060a3b2a26f117827090697eb599cd51a44125e6)) * pod analyzer catches errors when containers are in Terminated state ([#1438](https://github.com/k8sgpt-ai/k8sgpt/issues/1438)) ([dceda9a](https://github.com/k8sgpt-ai/k8sgpt/commit/dceda9a6a16a914b916c478ecd0b4c8ed0e19c40)) ## [0.4.9](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.8...v0.4.9) (2025-04-08) ### Other * **deps:** pin dependencies ([#1440](https://github.com/k8sgpt-ai/k8sgpt/issues/1440)) ([a5574ee](https://github.com/k8sgpt-ai/k8sgpt/commit/a5574ee49d530960a515c419f4875cf02cb36fb3)) * fixing ([#1437](https://github.com/k8sgpt-ai/k8sgpt/issues/1437)) ([f68ff0e](https://github.com/k8sgpt-ai/k8sgpt/commit/f68ff0efee9bad5f8368c83800611fa9acbc53d7)) ## [0.4.8](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.7...v0.4.8) (2025-04-07) ### Other * removed krew release ([#1434](https://github.com/k8sgpt-ai/k8sgpt/issues/1434)) ([39ae2aa](https://github.com/k8sgpt-ai/k8sgpt/commit/39ae2aa6351d6a77e0b45ad15b0d10b86a33f3be)) ## [0.4.7](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.6...v0.4.7) (2025-04-07) ### Other * **deps:** update actions/upload-artifact digest to ea165f8 ([#1425](https://github.com/k8sgpt-ai/k8sgpt/issues/1425)) ([9bffc7c](https://github.com/k8sgpt-ai/k8sgpt/commit/9bffc7cff776733f6d05669e6c02f594ee2db261)) * fixing build ([#1431](https://github.com/k8sgpt-ai/k8sgpt/issues/1431)) ([c5fe2c6](https://github.com/k8sgpt-ai/k8sgpt/commit/c5fe2c68d18d4fd713b3e638066327ad586d1871)) ## [0.4.6](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.5...v0.4.6) (2025-04-07) ### Other * **deps:** pin docker/build-push-action action to 471d1dc ([#1428](https://github.com/k8sgpt-ai/k8sgpt/issues/1428)) ([5086ccd](https://github.com/k8sgpt-ai/k8sgpt/commit/5086ccd65942ebb9a37bd2c3a48d16c4be99e8c1)) * fixing docker build push action ([#1426](https://github.com/k8sgpt-ai/k8sgpt/issues/1426)) ([1681aad](https://github.com/k8sgpt-ai/k8sgpt/commit/1681aadac106c608de9774ebfd7ea9df20eed482)) * updated actor for login ([#1430](https://github.com/k8sgpt-ai/k8sgpt/issues/1430)) ([b626102](https://github.com/k8sgpt-ai/k8sgpt/commit/b6261026f8b41e505359a52c18bebec7ef5079f9)) ## [0.4.5](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.4...v0.4.5) (2025-04-07) ### Other * fix workflows ([#1423](https://github.com/k8sgpt-ai/k8sgpt/issues/1423)) ([3dbc9e1](https://github.com/k8sgpt-ai/k8sgpt/commit/3dbc9e1a20a3a55971733d990ecd39e798a804e9)) ## [0.4.4](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.3...v0.4.4) (2025-04-06) ### Other * **deps:** update docker/setup-buildx-action digest to b5ca514 ([#1371](https://github.com/k8sgpt-ai/k8sgpt/issues/1371)) ([d4de5d9](https://github.com/k8sgpt-ai/k8sgpt/commit/d4de5d9e3fdd1cc4c7d6fc067a7426fef1d32c1d)) * **deps:** update module github.com/docker/docker to v28 ([#1376](https://github.com/k8sgpt-ai/k8sgpt/issues/1376)) ([68ddac0](https://github.com/k8sgpt-ai/k8sgpt/commit/68ddac008955933ffa27c2c4e46d286d9a26e100)) * updating deps ([#1422](https://github.com/k8sgpt-ai/k8sgpt/issues/1422)) ([5b7fb7e](https://github.com/k8sgpt-ai/k8sgpt/commit/5b7fb7e6199635e109c1bf7355bc11ff6f60071b)) ## [0.4.3](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.2...v0.4.3) (2025-04-04) ### Bug Fixes * **deps:** update module gopkg.in/yaml.v2 to v3 ([#1363](https://github.com/k8sgpt-ai/k8sgpt/issues/1363)) ([e4861e9](https://github.com/k8sgpt-ai/k8sgpt/commit/e4861e9e2d631652b82768567afb9ba174114134)) * prometheus UTF8Validation ([#1404](https://github.com/k8sgpt-ai/k8sgpt/issues/1404)) ([3c353b0](https://github.com/k8sgpt-ai/k8sgpt/commit/3c353b0e931028f3be3b229518cf86d24422a29d)) ### Other * added new AmazonBedrock model ([#1390](https://github.com/k8sgpt-ai/k8sgpt/issues/1390)) ([ad2c90a](https://github.com/k8sgpt-ai/k8sgpt/commit/ad2c90a129074a13dac4fdd8e918d8e26159c7a1)) * **deps:** pin golangci/golangci-lint-action action to 1481404 ([#1415](https://github.com/k8sgpt-ai/k8sgpt/issues/1415)) ([e231032](https://github.com/k8sgpt-ai/k8sgpt/commit/e231032e1bec1d2d25cb03b35e701aa86a61d5ee)) * **deps:** update goreleaser/goreleaser-action digest to 9c156ee ([#1411](https://github.com/k8sgpt-ai/k8sgpt/issues/1411)) ([c823de1](https://github.com/k8sgpt-ai/k8sgpt/commit/c823de12e6b6efcf9f5639665aac602ed85ae31d)) * linter ([#1414](https://github.com/k8sgpt-ai/k8sgpt/issues/1414)) ([f0b18cf](https://github.com/k8sgpt-ai/k8sgpt/commit/f0b18cfb1cd418b94b448d3b9de43f03841c92bb)) ### Docs * add table of contents and cleanup ([#1413](https://github.com/k8sgpt-ai/k8sgpt/issues/1413)) ([a31d07c](https://github.com/k8sgpt-ai/k8sgpt/commit/a31d07c802694d3455b665382ff12a2abc3e0ef7)) * remove extra dollar sign in README.md ([#1410](https://github.com/k8sgpt-ai/k8sgpt/issues/1410)) ([a962741](https://github.com/k8sgpt-ai/k8sgpt/commit/a962741220bf98e159f14895d01cd596a7691f87)) ## [0.4.2](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.1...v0.4.2) (2025-03-28) ### Features * old sonnet ([#1408](https://github.com/k8sgpt-ai/k8sgpt/issues/1408)) ([e5817f9](https://github.com/k8sgpt-ai/k8sgpt/commit/e5817f9e557f4f97b016a0a7b7674342c3a1773e)) ### Bug Fixes * **deps:** update k8s.io/utils digest to 1f6e0b7 ([#1405](https://github.com/k8sgpt-ai/k8sgpt/issues/1405)) ([f5eaf81](https://github.com/k8sgpt-ai/k8sgpt/commit/f5eaf817f0cf2b732013e67e94c758a225c35ba6)) ### Other * **deps:** update actions/setup-go digest to 0aaccfd ([#1401](https://github.com/k8sgpt-ai/k8sgpt/issues/1401)) ([81d4aaf](https://github.com/k8sgpt-ai/k8sgpt/commit/81d4aaf402647bf4bcbc618fd82f9518cf3a5b4d)) * **deps:** update actions/upload-artifact digest to ea165f8 ([#1402](https://github.com/k8sgpt-ai/k8sgpt/issues/1402)) ([eb381b8](https://github.com/k8sgpt-ai/k8sgpt/commit/eb381b8087bbb3216d9bcdcc88a71fbad9e31e41)) * **deps:** update docker/login-action digest to 74a5d14 ([#1397](https://github.com/k8sgpt-ai/k8sgpt/issues/1397)) ([fdf8e7a](https://github.com/k8sgpt-ai/k8sgpt/commit/fdf8e7a95a6667b782e1e347a3b1d2fb0f2aafde)) * fix error ([#1403](https://github.com/k8sgpt-ai/k8sgpt/issues/1403)) ([288ca86](https://github.com/k8sgpt-ai/k8sgpt/commit/288ca862b3aaf942e58aa0dad0e15e2fda84780f)) ## [0.4.1](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.0...v0.4.1) (2025-03-17) ### Features * add amazon bedrock nova pro and nova lite models ([#1383](https://github.com/k8sgpt-ai/k8sgpt/issues/1383)) ([aa1e237](https://github.com/k8sgpt-ai/k8sgpt/commit/aa1e237ebb8c816383561c9b3e6a1ca0ddea8f78)) * add custom restful backend for complex scenarios (e.g, rag) ([#1228](https://github.com/k8sgpt-ai/k8sgpt/issues/1228)) ([7540e00](https://github.com/k8sgpt-ai/k8sgpt/commit/7540e0084e0c0c44fc52ed9a906b76f9f2e6a981)) ### Bug Fixes * **deps:** update default model to gpt-4o for improved performance and cost efficiency ([#1332](https://github.com/k8sgpt-ai/k8sgpt/issues/1332)) ([4e39cb6](https://github.com/k8sgpt-ai/k8sgpt/commit/4e39cb65b3a7fc0d1c057c647794346e072d3fd0)) * **deps:** update module golang.org/x/net to v0.36.0 [security] ([#1395](https://github.com/k8sgpt-ai/k8sgpt/issues/1395)) ([eb7b36a](https://github.com/k8sgpt-ai/k8sgpt/commit/eb7b36aa2764bc460ffc29a0aee18abe3631c2ed)) ### Other * **deps:** update actions/setup-go digest to f111f33 ([#1364](https://github.com/k8sgpt-ai/k8sgpt/issues/1364)) ([f2fdfd8](https://github.com/k8sgpt-ai/k8sgpt/commit/f2fdfd8dcaae6f57378d50396c4746d738d38bf2)) * **deps:** update goreleaser/goreleaser-action digest to 90a3faa ([#1308](https://github.com/k8sgpt-ai/k8sgpt/issues/1308)) ([d6d2e3b](https://github.com/k8sgpt-ai/k8sgpt/commit/d6d2e3bc4254877c8af61aba7386706e942e3fe9)) * **deps:** update softprops/action-gh-release digest to c95fe14 ([#1359](https://github.com/k8sgpt-ai/k8sgpt/issues/1359)) ([db5e517](https://github.com/k8sgpt-ai/k8sgpt/commit/db5e517dbb23a4cb0f203427744f4007d6e9faa8)) ## [0.4.0](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.50...v0.4.0) (2025-03-06) ### ⚠ BREAKING CHANGES * Removal of Trivy ([#1386](https://github.com/k8sgpt-ai/k8sgpt/issues/1386)) ### Features * Removal of Trivy ([#1386](https://github.com/k8sgpt-ai/k8sgpt/issues/1386)) ([d1b2227](https://github.com/k8sgpt-ai/k8sgpt/commit/d1b2227ff9a8ef42bf63c83e289fbd801706821e)) ### Bug Fixes * [Bug] Filter PolicyReport ignores namespace flag ([#1355](https://github.com/k8sgpt-ai/k8sgpt/issues/1355)) ([9dcb21e](https://github.com/k8sgpt-ai/k8sgpt/commit/9dcb21e160233eb120ccf50f9b9b80c145d0e01a)) ### Other * Adding region ([#1388](https://github.com/k8sgpt-ai/k8sgpt/issues/1388)) ([4f4f4f1](https://github.com/k8sgpt-ai/k8sgpt/commit/4f4f4f13a065ca7add283088c93777f78dcea228)) * **deps:** update actions/upload-artifact digest to 4cec3d8 ([#1378](https://github.com/k8sgpt-ai/k8sgpt/issues/1378)) ([093975e](https://github.com/k8sgpt-ai/k8sgpt/commit/093975e50ddadeab70a7c4f544df8351ac9758a2)) * **deps:** update codecov/codecov-action digest to 0565863 ([#1387](https://github.com/k8sgpt-ai/k8sgpt/issues/1387)) ([2a6f485](https://github.com/k8sgpt-ai/k8sgpt/commit/2a6f48500c4567519453fc51ea070f5e407d3cfb)) * **deps:** update docker/build-push-action digest to 471d1dc ([#1358](https://github.com/k8sgpt-ai/k8sgpt/issues/1358)) ([f2e3b9a](https://github.com/k8sgpt-ai/k8sgpt/commit/f2e3b9a8a72c4df32713197e50756e37e1302ff9)) * remediating security issue ([#1381](https://github.com/k8sgpt-ai/k8sgpt/issues/1381)) ([1f95358](https://github.com/k8sgpt-ai/k8sgpt/commit/1f953585c91f8a208db3b37440e4d458b8d821eb)) ## [0.3.50](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.49...v0.3.50) (2025-02-24) ### Features * rework to how bedrock data models are structured and accessed ([#1369](https://github.com/k8sgpt-ai/k8sgpt/issues/1369)) ([7dadea2](https://github.com/k8sgpt-ai/k8sgpt/commit/7dadea257007df64148f1e47f7960d1d30df67b2)) ## [0.3.49](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.48...v0.3.49) (2025-02-20) ### Bug Fixes * **deps:** update all non-major dependencies ([#1335](https://github.com/k8sgpt-ai/k8sgpt/issues/1335)) ([8cd3b29](https://github.com/k8sgpt-ai/k8sgpt/commit/8cd3b2985e4cd61711497fb0436e72b6b8aa3162)) * **deps:** update k8s.io/utils digest to 24370be ([#1344](https://github.com/k8sgpt-ai/k8sgpt/issues/1344)) ([fcc8563](https://github.com/k8sgpt-ai/k8sgpt/commit/fcc8563e4eba9bf45d49901b7287d311b93372c2)) * **deps:** update module golang.org/x/net to v0.33.0 [security] ([#1354](https://github.com/k8sgpt-ai/k8sgpt/issues/1354)) ([5de4f77](https://github.com/k8sgpt-ai/k8sgpt/commit/5de4f7704a856fd7db7b2f800bda40c5beb9333b)) * **deps:** update module gopkg.in/yaml.v2 to v3 ([#1336](https://github.com/k8sgpt-ai/k8sgpt/issues/1336)) ([19abbef](https://github.com/k8sgpt-ai/k8sgpt/commit/19abbef9a3112ceb060ac3fd772e2e4f62f19f84)) * prevent npe by handling checking error in NewAnalysis call ([#1365](https://github.com/k8sgpt-ai/k8sgpt/issues/1365)) ([83672fa](https://github.com/k8sgpt-ai/k8sgpt/commit/83672fa768887dd1c6f4dc12a92c3444f100c4f6)) ### Other * **deps:** update actions/setup-go digest to 3041bf5 ([#1347](https://github.com/k8sgpt-ai/k8sgpt/issues/1347)) ([939e067](https://github.com/k8sgpt-ai/k8sgpt/commit/939e0672aaaa5538cd58bb171f1e5d1c07831651)) * **deps:** update actions/upload-artifact digest to 65c4c4a ([#1350](https://github.com/k8sgpt-ai/k8sgpt/issues/1350)) ([c506a4b](https://github.com/k8sgpt-ai/k8sgpt/commit/c506a4b441e24052398c00c93d96806cec1b9f75)) * **deps:** update codecov/codecov-action digest to 13ce06b ([#1342](https://github.com/k8sgpt-ai/k8sgpt/issues/1342)) ([990d723](https://github.com/k8sgpt-ai/k8sgpt/commit/990d7239091b368178e06af60e4dc0e897fc8236)) * **deps:** update docker/setup-buildx-action digest to 6524bf6 ([#1349](https://github.com/k8sgpt-ai/k8sgpt/issues/1349)) ([2918556](https://github.com/k8sgpt-ai/k8sgpt/commit/2918556793316ea4f5a319c9aa51c1fec12ede85)) * fix typo in "completion" ([#1362](https://github.com/k8sgpt-ai/k8sgpt/issues/1362)) ([06b8f78](https://github.com/k8sgpt-ai/k8sgpt/commit/06b8f78150308c1f6023747fa34826e038d6bc3a)) ### Docs * fix broken schema link in README.md ([#1373](https://github.com/k8sgpt-ai/k8sgpt/issues/1373)) ([076ca2f](https://github.com/k8sgpt-ai/k8sgpt/commit/076ca2f14832cf83e43c465c377ef21825218b2f)) ## [0.3.48](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.47...v0.3.48) (2024-12-04) ### Features * fixed missing cache params ([#1340](https://github.com/k8sgpt-ai/k8sgpt/issues/1340)) ([1363219](https://github.com/k8sgpt-ai/k8sgpt/commit/1363219b1b94e157ef03c53eba8838b7cef559b4)) ## [0.3.47](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.46...v0.3.47) (2024-12-02) ### Features * add new AWS Bedrock model ids ([#1330](https://github.com/k8sgpt-ai/k8sgpt/issues/1330)) ([a12aa07](https://github.com/k8sgpt-ai/k8sgpt/commit/a12aa07b1a2e34c5106b7b930b29b0c97b172dc4)) * adds interplex as a caching provider ([#1328](https://github.com/k8sgpt-ai/k8sgpt/issues/1328)) ([d6d80ee](https://github.com/k8sgpt-ai/k8sgpt/commit/d6d80ee86083643d9b91457791bfc77ef475e82e)) * dump ([#1322](https://github.com/k8sgpt-ai/k8sgpt/issues/1322)) ([da266b3](https://github.com/k8sgpt-ai/k8sgpt/commit/da266b3c82ca8b3e96461be688a9f30e408568fe)) ### Bug Fixes * add maxTokens to serve mode ([#1280](https://github.com/k8sgpt-ai/k8sgpt/issues/1280)) ([a50375c](https://github.com/k8sgpt-ai/k8sgpt/commit/a50375c9605a87546a0fcbcacabe5482fdfa1c2c)) * **deps:** update all non-major dependencies ([#1323](https://github.com/k8sgpt-ai/k8sgpt/issues/1323)) ([b3f60b2](https://github.com/k8sgpt-ai/k8sgpt/commit/b3f60b2d2018d4bede3918adcb3547ef2acf6688)) * **deps:** update module gopkg.in/yaml.v2 to v3 ([#1303](https://github.com/k8sgpt-ai/k8sgpt/issues/1303)) ([2da0573](https://github.com/k8sgpt-ai/k8sgpt/commit/2da057360b378d34126e1480ade0686f104e3ace)) * **deps:** update module gopkg.in/yaml.v2 to v3 ([#1321](https://github.com/k8sgpt-ai/k8sgpt/issues/1321)) ([69c67bd](https://github.com/k8sgpt-ai/k8sgpt/commit/69c67bd1d9d4404816a8b7a00c98499729f2185f)) * **deps:** update module gopkg.in/yaml.v2 to v3 ([#1326](https://github.com/k8sgpt-ai/k8sgpt/issues/1326)) ([5514ebb](https://github.com/k8sgpt-ai/k8sgpt/commit/5514ebb53b79b5bac0fc861ffdebc9399fe87b62)) * update OpenAI API key generation URL to reflect new platform link ([#1331](https://github.com/k8sgpt-ai/k8sgpt/issues/1331)) ([ec5e42b](https://github.com/k8sgpt-ai/k8sgpt/commit/ec5e42b8f43e90632bb62dd89cc6aa3665e0f60d)) ### Other * **deps:** update all non-major dependencies ([#1327](https://github.com/k8sgpt-ai/k8sgpt/issues/1327)) ([a841568](https://github.com/k8sgpt-ai/k8sgpt/commit/a841568a9c4c0012291cf8f4248250192b72a383)) * **deps:** update codecov/codecov-action action to v5 ([#1324](https://github.com/k8sgpt-ai/k8sgpt/issues/1324)) ([cb1e1ff](https://github.com/k8sgpt-ai/k8sgpt/commit/cb1e1ffede1d3086d54157142c6803341e560ca8)) * **deps:** update codecov/codecov-action digest to 015f24e ([#1325](https://github.com/k8sgpt-ai/k8sgpt/issues/1325)) ([4d7eb0f](https://github.com/k8sgpt-ai/k8sgpt/commit/4d7eb0f6226fc50f58b5c2fff7534dd16e2ca378)) * **deps:** update docker/build-push-action action to v6 ([#1294](https://github.com/k8sgpt-ai/k8sgpt/issues/1294)) ([f37d923](https://github.com/k8sgpt-ai/k8sgpt/commit/f37d92391877819c6d26a993ab58bc0c49fb3b66)) * **deps:** update docker/build-push-action digest to 48aba3b ([#1333](https://github.com/k8sgpt-ai/k8sgpt/issues/1333)) ([c21ba86](https://github.com/k8sgpt-ai/k8sgpt/commit/c21ba86237db651086c0a37abc3454db513e505b)) * **deps:** update rajatjindal/krew-release-bot action to v0.0.47 ([#1317](https://github.com/k8sgpt-ai/k8sgpt/issues/1317)) ([896a53b](https://github.com/k8sgpt-ai/k8sgpt/commit/896a53be8394c490e2d34f151de44c3663dddf5b)) ## [0.3.46](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.45...v0.3.46) (2024-11-10) ### Features * reverting the cncf runners ([#1319](https://github.com/k8sgpt-ai/k8sgpt/issues/1319)) ([ad86e7a](https://github.com/k8sgpt-ai/k8sgpt/commit/ad86e7aa39995c492437627dbd9f89f152f11f2c)) * switching to higher spec runners ([#1312](https://github.com/k8sgpt-ai/k8sgpt/issues/1312)) ([5f7d9de](https://github.com/k8sgpt-ai/k8sgpt/commit/5f7d9de46a521463cedc901b729fe27f8d86f381)) * testupdate ([#1315](https://github.com/k8sgpt-ai/k8sgpt/issues/1315)) ([7dcdfc8](https://github.com/k8sgpt-ai/k8sgpt/commit/7dcdfc83d2461e4342ded5fa80493936b70f64a1)) * updated runners to enterprise ([#1318](https://github.com/k8sgpt-ai/k8sgpt/issues/1318)) ([1ae70e8](https://github.com/k8sgpt-ai/k8sgpt/commit/1ae70e806e2609c8fb964f0a577304d07b365cae)) ### Other * **deps:** update actions/setup-go digest to 41dfa10 ([#1284](https://github.com/k8sgpt-ai/k8sgpt/issues/1284)) ([2ce8450](https://github.com/k8sgpt-ai/k8sgpt/commit/2ce8450e03986904a7ffe7afac4b5ba777c67c57)) * **deps:** update softprops/action-gh-release action to v2 ([#1295](https://github.com/k8sgpt-ai/k8sgpt/issues/1295)) ([b6b3d0c](https://github.com/k8sgpt-ai/k8sgpt/commit/b6b3d0c8566b0dbd9cb0e5f59c8493e4343e0106)) ## [0.3.45](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.44...v0.3.45) (2024-11-10) ### Features * free disk ([#1313](https://github.com/k8sgpt-ai/k8sgpt/issues/1313)) ([783cd1c](https://github.com/k8sgpt-ai/k8sgpt/commit/783cd1cfc66f8e4489e5006529745d8caf38cfd4)) ## [0.3.44](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.43...v0.3.44) (2024-11-09) ### Features * test revert runner on release job ([#1310](https://github.com/k8sgpt-ai/k8sgpt/issues/1310)) ([cc9b3ea](https://github.com/k8sgpt-ai/k8sgpt/commit/cc9b3ea6579c6190629e0fac48e37e0eba650158)) ## [0.3.43](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.42...v0.3.43) (2024-11-05) ### Bug Fixes * **deps:** update module gopkg.in/yaml.v2 to v3 ([#1296](https://github.com/k8sgpt-ai/k8sgpt/issues/1296)) ([2f75986](https://github.com/k8sgpt-ai/k8sgpt/commit/2f759865b6fc5ae143c8f5e89a306abc89d4de27)) ### Other * **deps:** update dependency ubuntu to v24 ([#1293](https://github.com/k8sgpt-ai/k8sgpt/issues/1293)) ([c67add3](https://github.com/k8sgpt-ai/k8sgpt/commit/c67add30c64257ac6258dec93193e3201ba8c4ab)) ## [0.3.42](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.41...v0.3.42) (2024-11-04) ### Features * add stats option to analyze command for performance insights ([#1237](https://github.com/k8sgpt-ai/k8sgpt/issues/1237)) ([3eec9bb](https://github.com/k8sgpt-ai/k8sgpt/commit/3eec9bbb05b2f0717437cc4a2ec786594ece1cc3)) * error from events for STS analyzer ([#1256](https://github.com/k8sgpt-ai/k8sgpt/issues/1256)) ([d8fad95](https://github.com/k8sgpt-ai/k8sgpt/commit/d8fad956f45a4dd668647379bb0295e169faeac6)) ### Bug Fixes * [Bug] Make lint command is not working ([#1282](https://github.com/k8sgpt-ai/k8sgpt/issues/1282)) ([87565a0](https://github.com/k8sgpt-ai/k8sgpt/commit/87565a0bcce7087114798c3a32877894c8a9dcee)) * add providerId to serve mode ([#1260](https://github.com/k8sgpt-ai/k8sgpt/issues/1260)) ([da0764d](https://github.com/k8sgpt-ai/k8sgpt/commit/da0764d951ca76cb7007c412f8efa794619c20ba)) * **deps:** update all non-major dependencies ([#1291](https://github.com/k8sgpt-ai/k8sgpt/issues/1291)) ([14e0f19](https://github.com/k8sgpt-ai/k8sgpt/commit/14e0f19b12189052b03d551e409b407fd0b6bd30)) * **deps:** update k8s.io/utils digest to 49e7df5 ([#1259](https://github.com/k8sgpt-ai/k8sgpt/issues/1259)) ([7785dd1](https://github.com/k8sgpt-ai/k8sgpt/commit/7785dd12a0245a33af25dedd2fbb5f4178b5cda9)) * **deps:** update module buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go to v1.35.1-20240920204244-7a91c8620515.1 ([#1274](https://github.com/k8sgpt-ai/k8sgpt/issues/1274)) ([9f39abf](https://github.com/k8sgpt-ai/k8sgpt/commit/9f39abf89e4e92009f5e138d9b01d11c60ac135c)) * **deps:** update module cloud.google.com/go/storage to v1.44.0 ([#1265](https://github.com/k8sgpt-ai/k8sgpt/issues/1265)) ([4143e9f](https://github.com/k8sgpt-ai/k8sgpt/commit/4143e9fd524bed3179524d949b7b0f92c02ecd11)) * **deps:** update module github.com/adrg/xdg to v0.5.0 ([#1262](https://github.com/k8sgpt-ai/k8sgpt/issues/1262)) ([98237b6](https://github.com/k8sgpt-ai/k8sgpt/commit/98237b6408521ee7afc05fcaed2f78ba79e77144)) * **deps:** update module github.com/aquasecurity/trivy-operator to v0.22.0 ([#1034](https://github.com/k8sgpt-ai/k8sgpt/issues/1034)) ([037e745](https://github.com/k8sgpt-ai/k8sgpt/commit/037e745c6f667830f0e1d531ce4bbd07083ef972)) * **deps:** update module github.com/aws/aws-sdk-go to v1.55.5 ([#1263](https://github.com/k8sgpt-ai/k8sgpt/issues/1263)) ([0148a5b](https://github.com/k8sgpt-ai/k8sgpt/commit/0148a5b3549cbdb6c6e5832dc01aab044b90ddc9)) * **deps:** update module github.com/azure/azure-sdk-for-go/sdk/azidentity to v1.8.0 ([#1264](https://github.com/k8sgpt-ai/k8sgpt/issues/1264)) ([3613585](https://github.com/k8sgpt-ai/k8sgpt/commit/36135857ac55e126b3a6c4533a000cb0b7f32c6b)) * **deps:** update module github.com/azure/azure-sdk-for-go/sdk/storage/azblob to v1.4.1 ([#1275](https://github.com/k8sgpt-ai/k8sgpt/issues/1275)) ([c9b11b6](https://github.com/k8sgpt-ai/k8sgpt/commit/c9b11b6eee00d0269a4d48ad2e4be5458436b51d)) * **deps:** update module github.com/cohere-ai/cohere-go/v2 to v2.12.0 ([#1276](https://github.com/k8sgpt-ai/k8sgpt/issues/1276)) ([7a3fb3c](https://github.com/k8sgpt-ai/k8sgpt/commit/7a3fb3cf6777d5b0babf00455c3833a47bb1bfdb)) * **deps:** update module github.com/google/generative-ai-go to v0.18.0 ([#1278](https://github.com/k8sgpt-ai/k8sgpt/issues/1278)) ([ad349ae](https://github.com/k8sgpt-ai/k8sgpt/commit/ad349ae263f226e300f60dd092729c5a3bf61dbe)) * rename watsonxai to ibmwatsonxai ([#1234](https://github.com/k8sgpt-ai/k8sgpt/issues/1234)) ([5ff6dc9](https://github.com/k8sgpt-ai/k8sgpt/commit/5ff6dc9be5218e47839c4ac5e8f3458b40eb9c88)) ### Other * **deps:** update actions/checkout digest to 11bd719 ([#1283](https://github.com/k8sgpt-ai/k8sgpt/issues/1283)) ([0cfecbd](https://github.com/k8sgpt-ai/k8sgpt/commit/0cfecbdd87586fd138cc63c4e7a26d54e7ed83a8)) * **deps:** update actions/checkout digest to eef6144 ([#1270](https://github.com/k8sgpt-ai/k8sgpt/issues/1270)) ([72eb815](https://github.com/k8sgpt-ai/k8sgpt/commit/72eb8159fb4a2284cf43eb6a5f3de7bed10c6224)) * **deps:** update actions/upload-artifact digest to b4b15b8 ([#1272](https://github.com/k8sgpt-ai/k8sgpt/issues/1272)) ([911d578](https://github.com/k8sgpt-ai/k8sgpt/commit/911d578bf006253d10fe21d96888ddf34a8b4691)) * **deps:** update anchore/sbom-action action to v0.17.2 ([#1248](https://github.com/k8sgpt-ai/k8sgpt/issues/1248)) ([04582d8](https://github.com/k8sgpt-ai/k8sgpt/commit/04582d85160055da30e4e00fd3c6ca69d1decd1a)) * **deps:** update anchore/sbom-action action to v0.17.4 ([#1273](https://github.com/k8sgpt-ai/k8sgpt/issues/1273)) ([c128bf7](https://github.com/k8sgpt-ai/k8sgpt/commit/c128bf7942e380fcab5e9771f405471198e388fe)) * **deps:** update anchore/sbom-action action to v0.17.6 ([#1285](https://github.com/k8sgpt-ai/k8sgpt/issues/1285)) ([173e4dc](https://github.com/k8sgpt-ai/k8sgpt/commit/173e4dc5ac6265af4a3538556220d3a43ab721f7)) * **deps:** update codecov/codecov-action action to v4 ([#1292](https://github.com/k8sgpt-ai/k8sgpt/issues/1292)) ([c1a38c2](https://github.com/k8sgpt-ai/k8sgpt/commit/c1a38c2b35a0bfa772b88f15843c9354b0345284)) * **deps:** update docker/setup-buildx-action digest to c47758b ([#1213](https://github.com/k8sgpt-ai/k8sgpt/issues/1213)) ([161bc11](https://github.com/k8sgpt-ai/k8sgpt/commit/161bc11294d5094533068cf7af9880795a61536e)) * **deps:** update golang docker tag to v1.23 ([#1254](https://github.com/k8sgpt-ai/k8sgpt/issues/1254)) ([b62b7db](https://github.com/k8sgpt-ai/k8sgpt/commit/b62b7dbe3c9cd02b81f6a0111bca939034c5cc9f)) * **deps:** update module github.com/docker/docker to v27.3.1+incompatible ([#1225](https://github.com/k8sgpt-ai/k8sgpt/issues/1225)) ([9c1927b](https://github.com/k8sgpt-ai/k8sgpt/commit/9c1927b4975fa8132fbc24dd96a5737819855544)) * renovate.json ([#1290](https://github.com/k8sgpt-ai/k8sgpt/issues/1290)) ([458fcfe](https://github.com/k8sgpt-ai/k8sgpt/commit/458fcfe8d330523781d32af680febc2a0c0525a2)) ## [0.3.41](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.40...v0.3.41) (2024-09-22) ### Features * add custom-analyzer cmd ([#1207](https://github.com/k8sgpt-ai/k8sgpt/issues/1207)) ([db26d24](https://github.com/k8sgpt-ai/k8sgpt/commit/db26d24ac607534ce78c1c82f3e1d4e5dde17578)) * add event failure handling in service analyzer ([#1132](https://github.com/k8sgpt-ai/k8sgpt/issues/1132)) ([a4e44d5](https://github.com/k8sgpt-ai/k8sgpt/commit/a4e44d59e3ee63714cfd144228299e4f24ac3691)) * added support for A21 and Amazon Titan models via bedrock api ([#1101](https://github.com/k8sgpt-ai/k8sgpt/issues/1101)) ([4f3ecf0](https://github.com/k8sgpt-ai/k8sgpt/commit/4f3ecf008351075068738e930ff3a657f597654a)) * adding a query mode for the schednex scheduler ([#1257](https://github.com/k8sgpt-ai/k8sgpt/issues/1257)) ([53465d5](https://github.com/k8sgpt-ai/k8sgpt/commit/53465d5c832ac490403a2698b80122ca06372df7)) * refactoring to the new schema ([#1219](https://github.com/k8sgpt-ai/k8sgpt/issues/1219)) ([02fa109](https://github.com/k8sgpt-ai/k8sgpt/commit/02fa109429d3c684079f5d488e7f517806fc1a09)) ### Bug Fixes * **deps:** update k8s.io/utils digest to 702e33f ([#1246](https://github.com/k8sgpt-ai/k8sgpt/issues/1246)) ([d30563d](https://github.com/k8sgpt-ai/k8sgpt/commit/d30563d8cdedb5bbf48735e49ebcb44440a5f0f5)) * **deps:** update module buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc-ecosystem/gateway/v2 to v2.22.0-20240807134501-ea98c104104d.1 ([#1186](https://github.com/k8sgpt-ai/k8sgpt/issues/1186)) ([8405778](https://github.com/k8sgpt-ai/k8sgpt/commit/8405778cb25429d2b42d7a3b50ec88b45961a57f)) * **deps:** update module github.com/docker/docker to v27.1.1+incompatible [security] ([#1220](https://github.com/k8sgpt-ai/k8sgpt/issues/1220)) ([3148b5c](https://github.com/k8sgpt-ai/k8sgpt/commit/3148b5c61d2ff57d67d966d6e915994d4aa8a844)) * **deps:** update module github.com/mittwald/go-helm-client to v0.12.12 ([#1226](https://github.com/k8sgpt-ai/k8sgpt/issues/1226)) ([7019d0b](https://github.com/k8sgpt-ai/k8sgpt/commit/7019d0b62f1bebbd4c2a251c98a2beb4975bf2fe)) * **deps:** update module github.com/mittwald/go-helm-client to v0.12.13 ([#1251](https://github.com/k8sgpt-ai/k8sgpt/issues/1251)) ([1dfd139](https://github.com/k8sgpt-ai/k8sgpt/commit/1dfd13973165bd2820aa8ca079e1ec656a5033f0)) * **deps:** update module github.com/schollz/progressbar/v3 to v3.15.0 ([#1227](https://github.com/k8sgpt-ai/k8sgpt/issues/1227)) ([025a069](https://github.com/k8sgpt-ai/k8sgpt/commit/025a069ff1582131cede63420aa535a3b550b7b7)) * disable adding multiple openai provider ([#1191](https://github.com/k8sgpt-ai/k8sgpt/issues/1191)) ([644581f](https://github.com/k8sgpt-ai/k8sgpt/commit/644581f4958f470cfb088a69a478db0ab91c1540)) * enabled auth add support watsonx backend ([#1190](https://github.com/k8sgpt-ai/k8sgpt/issues/1190)) ([d702209](https://github.com/k8sgpt-ai/k8sgpt/commit/d702209941480dce62b9622ea30fdb4a9e5ef083)) * helm chart security context rendering if empty ([#1235](https://github.com/k8sgpt-ai/k8sgpt/issues/1235)) ([be4ca86](https://github.com/k8sgpt-ai/k8sgpt/commit/be4ca86af07e832eb7832f7e5f83df8676bafd29)) * issue-1168, remove duplicate CVE ([#1230](https://github.com/k8sgpt-ai/k8sgpt/issues/1230)) ([8edb053](https://github.com/k8sgpt-ai/k8sgpt/commit/8edb053b3e88027880a75999eab19bed2176747f)) * segmentation violation during serve ([#1215](https://github.com/k8sgpt-ai/k8sgpt/issues/1215)) ([b7e5394](https://github.com/k8sgpt-ai/k8sgpt/commit/b7e5394caaabb43e01161618f7a6e9f4aa8f7408)) * set logger for controller-runtime ([#1211](https://github.com/k8sgpt-ai/k8sgpt/issues/1211)) ([8e37369](https://github.com/k8sgpt-ai/k8sgpt/commit/8e37369e5c6c96096b66179f22a27b2c0018c43a)) * typo ([#1244](https://github.com/k8sgpt-ai/k8sgpt/issues/1244)) ([e02c0dd](https://github.com/k8sgpt-ai/k8sgpt/commit/e02c0ddd2d9f9a6fae8a57514468f26fe72b567a)) ### Other * **deps:** update actions/checkout digest to 692973e ([#1129](https://github.com/k8sgpt-ai/k8sgpt/issues/1129)) ([24ebeaf](https://github.com/k8sgpt-ai/k8sgpt/commit/24ebeaf3a748f2bf40c18ddcecaf8655b457048b)) * **deps:** update actions/upload-artifact digest to 5076954 ([#1239](https://github.com/k8sgpt-ai/k8sgpt/issues/1239)) ([e0e86ea](https://github.com/k8sgpt-ai/k8sgpt/commit/e0e86ea60f3811e8ee22fd9c28e91817c56104a2)) * **deps:** update actions/upload-artifact digest to 834a144 ([#1214](https://github.com/k8sgpt-ai/k8sgpt/issues/1214)) ([2a8a9b4](https://github.com/k8sgpt-ai/k8sgpt/commit/2a8a9b486714d780c0df3ecae8757534249731dc)) * **deps:** update anchore/sbom-action action to v0.17.1 ([#1224](https://github.com/k8sgpt-ai/k8sgpt/issues/1224)) ([f573819](https://github.com/k8sgpt-ai/k8sgpt/commit/f57381961fbc63305d9e9aa63e85a90a100ee553)) * **deps:** update dependency go to v1.23.1 ([#1176](https://github.com/k8sgpt-ai/k8sgpt/issues/1176)) ([453d5c3](https://github.com/k8sgpt-ai/k8sgpt/commit/453d5c37ddafd93c6fa194b5b4fc0794154eb8c1)) * **deps:** update docker/login-action digest to 9780b0c ([#1212](https://github.com/k8sgpt-ai/k8sgpt/issues/1212)) ([477ef15](https://github.com/k8sgpt-ai/k8sgpt/commit/477ef155d32f4d81ca3bee612644f51fc1098cdc)) ### Docs * update "CLI Installation" section in README.md ([#1126](https://github.com/k8sgpt-ai/k8sgpt/issues/1126)) ([#1127](https://github.com/k8sgpt-ai/k8sgpt/issues/1127)) ([b2b8682](https://github.com/k8sgpt-ai/k8sgpt/commit/b2b86826e55984c2b6aed6554869d7ce66a5f854)) ## [0.3.40](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.39...v0.3.40) (2024-08-04) ### Features * custom analysis paralelism ([#1203](https://github.com/k8sgpt-ai/k8sgpt/issues/1203)) ([f1b7b37](https://github.com/k8sgpt-ai/k8sgpt/commit/f1b7b37fb83937d5fad90d7b6b52f4a38823da9e)) * getting the error from status field for HPA analyzer ([#1164](https://github.com/k8sgpt-ai/k8sgpt/issues/1164)) ([a068310](https://github.com/k8sgpt-ai/k8sgpt/commit/a068310731d775beecede03a1709e541ffd68142)) * initial custom analysis server mode ([#1205](https://github.com/k8sgpt-ai/k8sgpt/issues/1205)) ([16d57e5](https://github.com/k8sgpt-ai/k8sgpt/commit/16d57e5a55c2084bf1580377ae52e2961cc84922)) ### Bug Fixes * add default maxToken value of watsonxai backend ([#1209](https://github.com/k8sgpt-ai/k8sgpt/issues/1209)) ([d43fd87](https://github.com/k8sgpt-ai/k8sgpt/commit/d43fd878ba04fec8ac8afe4a1c15272b7f21c951)) * auth update throw out exception ([#1193](https://github.com/k8sgpt-ai/k8sgpt/issues/1193)) ([391a3cd](https://github.com/k8sgpt-ai/k8sgpt/commit/391a3cd5adcbd90f37922332b4fad5ba5d813e5f)) * **deps:** update module cloud.google.com/go/storage to v1.43.0 ([#1198](https://github.com/k8sgpt-ai/k8sgpt/issues/1198)) ([8949f5b](https://github.com/k8sgpt-ai/k8sgpt/commit/8949f5bac3c69130e30103511fdb5ece66e1619f)) * **deps:** update module github.com/schollz/progressbar/v3 to v3.14.5 ([#1145](https://github.com/k8sgpt-ai/k8sgpt/issues/1145)) ([3547c48](https://github.com/k8sgpt-ai/k8sgpt/commit/3547c4808a846eb4392996afa20a84bdddf8e24f)) ### Other * **deps:** update anchore/sbom-action action to v0.17.0 ([#1197](https://github.com/k8sgpt-ai/k8sgpt/issues/1197)) ([407c855](https://github.com/k8sgpt-ai/k8sgpt/commit/407c855e147b73739e800310c926826344d36323)) ## [0.3.39](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.38...v0.3.39) (2024-07-18) ### Features * add label selector ([#1201](https://github.com/k8sgpt-ai/k8sgpt/issues/1201)) ([eb3b81f](https://github.com/k8sgpt-ai/k8sgpt/commit/eb3b81f1767c589474864992ae78001ab1b376a1)) * fix the custom-analysis printing ([#1195](https://github.com/k8sgpt-ai/k8sgpt/issues/1195)) ([b6dd2a1](https://github.com/k8sgpt-ai/k8sgpt/commit/b6dd2a1181b478a4fb8543ab7529ce595fa7d4a8)) * initial kyverno support ([#1200](https://github.com/k8sgpt-ai/k8sgpt/issues/1200)) ([5176759](https://github.com/k8sgpt-ai/k8sgpt/commit/5176759bd0fad8671164f9e75b31dec19f02bd54)) * skip k3s node type EtcdIsVoter ([#1167](https://github.com/k8sgpt-ai/k8sgpt/issues/1167)) ([4366ad9](https://github.com/k8sgpt-ai/k8sgpt/commit/4366ad97b80d2df0400e06e4b892fadab3939dc7)) ### Bug Fixes * **deps:** update k8s.io/utils digest to 18e509b ([#1183](https://github.com/k8sgpt-ai/k8sgpt/issues/1183)) ([0b90651](https://github.com/k8sgpt-ai/k8sgpt/commit/0b906511d5a9837c9a67cf819754c610b1becc5c)) * **deps:** update module buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go to v1.4.0-20240715142657-3785f0a44aae.2 ([#1196](https://github.com/k8sgpt-ai/k8sgpt/issues/1196)) ([f9edbf3](https://github.com/k8sgpt-ai/k8sgpt/commit/f9edbf34f3eb3e90528d04b1c470fd6ef15293ec)) * **deps:** update module github.com/ibm/watsonx-go to v1.0.1 ([#1187](https://github.com/k8sgpt-ai/k8sgpt/issues/1187)) ([34b6de3](https://github.com/k8sgpt-ai/k8sgpt/commit/34b6de34041ce253c1c680a7f5fe535b03a50da5)) * **deps:** update module github.com/prometheus/prometheus to v0.53.1 ([#1035](https://github.com/k8sgpt-ai/k8sgpt/issues/1035)) ([de9ef85](https://github.com/k8sgpt-ai/k8sgpt/commit/de9ef8587822814542661e0039b47ef65d902abb)) ### Other * **deps:** pin goreleaser/goreleaser-action action to 286f3b1 ([#1171](https://github.com/k8sgpt-ai/k8sgpt/issues/1171)) ([1a00aaf](https://github.com/k8sgpt-ai/k8sgpt/commit/1a00aafbb2f6f1482dfb3da7e96954b12ad5a4fd)) * **deps:** update actions/setup-go digest to 0a12ed9 ([#1182](https://github.com/k8sgpt-ai/k8sgpt/issues/1182)) ([593139c](https://github.com/k8sgpt-ai/k8sgpt/commit/593139cffb1982fe45ccc9403acc893f51064271)) * **deps:** update actions/upload-artifact digest to 0b2256b ([#1175](https://github.com/k8sgpt-ai/k8sgpt/issues/1175)) ([4b13727](https://github.com/k8sgpt-ai/k8sgpt/commit/4b13727ef579240adc2777d1126544fafb23b993)) * **deps:** update anchore/sbom-action action to v0.16.1 ([#1179](https://github.com/k8sgpt-ai/k8sgpt/issues/1179)) ([3e93409](https://github.com/k8sgpt-ai/k8sgpt/commit/3e9340925c3d59861b1a95d5c1bc08c19ec26e4a)) ## [0.3.38](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.37...v0.3.38) (2024-07-10) ### Features * add custom http headers to openai related api backends ([#1174](https://github.com/k8sgpt-ai/k8sgpt/issues/1174)) ([02e754e](https://github.com/k8sgpt-ai/k8sgpt/commit/02e754ed591742fccc5ff9a20c3e36e4475f6ec5)) * add Ollama backend ([#1065](https://github.com/k8sgpt-ai/k8sgpt/issues/1065)) ([b35dbd9](https://github.com/k8sgpt-ai/k8sgpt/commit/b35dbd9b09197994f041cda04f1a4e5fb316e468)) * add watsonx ai provider ([#1163](https://github.com/k8sgpt-ai/k8sgpt/issues/1163)) ([ce63821](https://github.com/k8sgpt-ai/k8sgpt/commit/ce63821bebbd87b2e058f5cf58a2cdd474b8fb58)) ### Bug Fixes * **deps:** update module buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc-ecosystem/gateway/v2 to v2.20.0-20240406062209-1cc152efbf5c.1 ([#1147](https://github.com/k8sgpt-ai/k8sgpt/issues/1147)) ([314f25a](https://github.com/k8sgpt-ai/k8sgpt/commit/314f25ac8bf5c3629474ece0eae6a3bda83099aa)) * **deps:** update module github.com/mittwald/go-helm-client to v0.12.10 ([#1177](https://github.com/k8sgpt-ai/k8sgpt/issues/1177)) ([fef8539](https://github.com/k8sgpt-ai/k8sgpt/commit/fef853966fc6e33dae0a9686fa767b36201c0228)) * **deps:** update module github.com/spf13/cobra to v1.8.1 ([#1161](https://github.com/k8sgpt-ai/k8sgpt/issues/1161)) ([a075792](https://github.com/k8sgpt-ai/k8sgpt/commit/a0757921191205398539a6ccc8dbfaa503db595f)) * **deps:** update module google.golang.org/grpc to v1.64.1 [security] ([#1178](https://github.com/k8sgpt-ai/k8sgpt/issues/1178)) ([dd20dbc](https://github.com/k8sgpt-ai/k8sgpt/commit/dd20dbc9829fc50f77ad6a32c3a10dcf221d2750)) ### Other * **deps:** update amannn/action-semantic-pull-request action to v5.5.3 ([#1172](https://github.com/k8sgpt-ai/k8sgpt/issues/1172)) ([27ac60a](https://github.com/k8sgpt-ai/k8sgpt/commit/27ac60aed296c3d9582f34e14c5985a4bccd991e)) * **deps:** update anchore/sbom-action action to v0.16.0 ([#1146](https://github.com/k8sgpt-ai/k8sgpt/issues/1146)) ([dd66355](https://github.com/k8sgpt-ai/k8sgpt/commit/dd6635579789ce65ee86dc1196e7dfde1b7d20e6)) * **deps:** update docker/build-push-action digest to ca052bb ([#1140](https://github.com/k8sgpt-ai/k8sgpt/issues/1140)) ([0c02160](https://github.com/k8sgpt-ai/k8sgpt/commit/0c0216096efde9c2c812ee90522c081f51c52631)) * **deps:** update docker/setup-buildx-action digest to 4fd8129 ([#1173](https://github.com/k8sgpt-ai/k8sgpt/issues/1173)) ([d4abb33](https://github.com/k8sgpt-ai/k8sgpt/commit/d4abb33b3c29d9a2e4dee094ea7be2bc5d1807d1)) * update brew installation note ([#1155](https://github.com/k8sgpt-ai/k8sgpt/issues/1155)) ([ab534d1](https://github.com/k8sgpt-ai/k8sgpt/commit/ab534d184fcd538f2ba10a6b5bf3a74c28d5fee6)) ## [0.3.37](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.36...v0.3.37) (2024-06-17) ### Other * **deps:** update reviewdog/action-golangci-lint digest to 7708105 ([#1157](https://github.com/k8sgpt-ai/k8sgpt/issues/1157)) ([7b1b633](https://github.com/k8sgpt-ai/k8sgpt/commit/7b1b63322ec7b0c0864682bc23be6e70c0ed7ec7)) * updated the goreleaser action ([#1160](https://github.com/k8sgpt-ai/k8sgpt/issues/1160)) ([9bace02](https://github.com/k8sgpt-ai/k8sgpt/commit/9bace02a6702a8af0e6511b51ffc38378e14d3cb)) ## [0.3.36](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.35...v0.3.36) (2024-06-17) ### Other * **deps:** update docker/login-action digest to 0d4c9c5 ([#1141](https://github.com/k8sgpt-ai/k8sgpt/issues/1141)) ([602d111](https://github.com/k8sgpt-ai/k8sgpt/commit/602d111d8568d38cda744d2b179ee2d3eb59ba02)) * **deps:** update goreleaser/goreleaser-action digest to 5742e2a ([#1153](https://github.com/k8sgpt-ai/k8sgpt/issues/1153)) ([55ae7c3](https://github.com/k8sgpt-ai/k8sgpt/commit/55ae7c32986100d4b0bab6dcaf7a52ac7b37aa5f)) * fixed the goreleaser file ([#1158](https://github.com/k8sgpt-ai/k8sgpt/issues/1158)) ([2382de4](https://github.com/k8sgpt-ai/k8sgpt/commit/2382de4c6f82de535b67c2752d7c502d0a8b2b66)) * update goreleaser ldflags ([#1154](https://github.com/k8sgpt-ai/k8sgpt/issues/1154)) ([aeae2ba](https://github.com/k8sgpt-ai/k8sgpt/commit/aeae2ba765c7db6e4953b5a93c54617f1dd85efa)) ## [0.3.35](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.34...v0.3.35) (2024-06-14) ### Features * add spec.template.spec.securityContext ([#1109](https://github.com/k8sgpt-ai/k8sgpt/issues/1109)) ([92dd1bd](https://github.com/k8sgpt-ai/k8sgpt/commit/92dd1bd8b08c5173f72a6c333f626c63aa05a1d3)) * support openai organization Id ([#1133](https://github.com/k8sgpt-ai/k8sgpt/issues/1133)) ([4867d39](https://github.com/k8sgpt-ai/k8sgpt/commit/4867d39c66a6c16906cd769a2055dea9f66f1ccb)) ### Other * updated goreleaser config ([#1149](https://github.com/k8sgpt-ai/k8sgpt/issues/1149)) ([c834c09](https://github.com/k8sgpt-ai/k8sgpt/commit/c834c099969f3e888f49f73fba6794387063a6fc)) ## [0.3.34](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.33...v0.3.34) (2024-06-14) ### Other * **deps:** update google-github-actions/release-please-action action to v4.1.1 ([#1143](https://github.com/k8sgpt-ai/k8sgpt/issues/1143)) ([63b63f7](https://github.com/k8sgpt-ai/k8sgpt/commit/63b63f7664277042188351073f269569bfec65bf)) * **deps:** update goreleaser/goreleaser-action digest to 5742e2a ([#1142](https://github.com/k8sgpt-ai/k8sgpt/issues/1142)) ([c101e8a](https://github.com/k8sgpt-ai/k8sgpt/commit/c101e8a3ea6d911d00ca2a51986edc5425a1042a)) ## [0.3.33](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.32...v0.3.33) (2024-06-13) ### Features * bump golang version to 1.22 ([#1117](https://github.com/k8sgpt-ai/k8sgpt/issues/1117)) ([6652fbe](https://github.com/k8sgpt-ai/k8sgpt/commit/6652fbe7cb6e581497e1d086e13397ff9e5b11be)) ### Bug Fixes * advisory k8sgpt ghsa 85rg 8m6h 825p ([#1139](https://github.com/k8sgpt-ai/k8sgpt/issues/1139)) ([728555c](https://github.com/k8sgpt-ai/k8sgpt/commit/728555c0effbf7a56221d625bcbbf62f74d14359)) * **deps:** typo in prometheus.go ([fad00ea](https://github.com/k8sgpt-ai/k8sgpt/commit/fad00eac4925351c4dc6fd6dd347fe2968f0b7a5)) * **deps:** typo in prometheus.go ([#1137](https://github.com/k8sgpt-ai/k8sgpt/issues/1137)) ([fad00ea](https://github.com/k8sgpt-ai/k8sgpt/commit/fad00eac4925351c4dc6fd6dd347fe2968f0b7a5)) * **deps:** update module github.com/aws/aws-sdk-go to v1.53.21 ([#1106](https://github.com/k8sgpt-ai/k8sgpt/issues/1106)) ([bdd470f](https://github.com/k8sgpt-ai/k8sgpt/commit/bdd470f9cae917f965badd22da7def4a7d64d2ae)) * **deps:** update module github.com/azure/azure-sdk-for-go/sdk/azidentity to v1.6.0 [security] ([#1138](https://github.com/k8sgpt-ai/k8sgpt/issues/1138)) ([3a89318](https://github.com/k8sgpt-ai/k8sgpt/commit/3a893184af50f8c822ac06ce0e20818eaec587b1)) ### Other * **deps:** update actions/setup-go digest to cdcb360 ([#1096](https://github.com/k8sgpt-ai/k8sgpt/issues/1096)) ([3452c0d](https://github.com/k8sgpt-ai/k8sgpt/commit/3452c0def68fd5352d2d09201f813f657245bd9f)) ## [0.3.32](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.31...v0.3.32) (2024-05-20) ### Bug Fixes * remove shorthand flag for topp option in add command ([#1115](https://github.com/k8sgpt-ai/k8sgpt/issues/1115)) ([e261c09](https://github.com/k8sgpt-ai/k8sgpt/commit/e261c09889359d5870acb9720ff033440f835f8f)) ## [0.3.31](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.30...v0.3.31) (2024-05-16) ### Features * implement Top-K sampling for improved user control ([#1110](https://github.com/k8sgpt-ai/k8sgpt/issues/1110)) ([eda5231](https://github.com/k8sgpt-ai/k8sgpt/commit/eda52312aef8113debbd770b8354c3a3cb1cc681)) * oci genai ([#1102](https://github.com/k8sgpt-ai/k8sgpt/issues/1102)) ([047afd4](https://github.com/k8sgpt-ai/k8sgpt/commit/047afd46d62d1bd1da1435550cbaf9daaca53aee)) * support AWS_PROFILE ([#1114](https://github.com/k8sgpt-ai/k8sgpt/issues/1114)) ([882c6f5](https://github.com/k8sgpt-ai/k8sgpt/commit/882c6f52252000da436e4fed9fd184b263f5a017)) ### Bug Fixes * **deps:** update k8s.io/utils digest to 0849a56 ([#1080](https://github.com/k8sgpt-ai/k8sgpt/issues/1080)) ([e894e77](https://github.com/k8sgpt-ai/k8sgpt/commit/e894e778e91d070448cd4a3f46dfc98dd588c9ed)) * **deps:** update module buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc-ecosystem/gateway/v2 to v2.19.1-20240406062209-1cc152efbf5c.1 ([#1070](https://github.com/k8sgpt-ai/k8sgpt/issues/1070)) ([24cff90](https://github.com/k8sgpt-ai/k8sgpt/commit/24cff90a0ca7488e48c94d13678529617c749aab)) * **deps:** update module buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go to v1.3.0-20240406062209-1cc152efbf5c.3 ([#1086](https://github.com/k8sgpt-ai/k8sgpt/issues/1086)) ([820cd2e](https://github.com/k8sgpt-ai/k8sgpt/commit/820cd2e16cbca2c89b56a4d2a69f95f3f5cd6c6b)) * **deps:** update module github.com/aws/aws-sdk-go to v1.51.32 ([#1083](https://github.com/k8sgpt-ai/k8sgpt/issues/1083)) ([75c2add](https://github.com/k8sgpt-ai/k8sgpt/commit/75c2addf66a54df57d0c0ac17f0b359f7612e446)) * **deps:** update module github.com/aws/aws-sdk-go to v1.52.3 ([#1094](https://github.com/k8sgpt-ai/k8sgpt/issues/1094)) ([3c48231](https://github.com/k8sgpt-ai/k8sgpt/commit/3c4823127ca04d1d280da6d932e951e6c3f71536)) * **deps:** update module github.com/azure/azure-sdk-for-go/sdk/azidentity to v1.5.2 ([#1084](https://github.com/k8sgpt-ai/k8sgpt/issues/1084)) ([bd695d0](https://github.com/k8sgpt-ai/k8sgpt/commit/bd695d0987e8ec12b44512c46bc5f2e5116076bd)) * **deps:** update module github.com/azure/azure-sdk-for-go/sdk/storage/azblob to v1.3.2 ([#1085](https://github.com/k8sgpt-ai/k8sgpt/issues/1085)) ([43953ff](https://github.com/k8sgpt-ai/k8sgpt/commit/43953ffa3412ae97b6d54ed14b94955d1b73feba)) * **deps:** update module github.com/cohere-ai/cohere-go/v2 to v2.7.3 ([#1087](https://github.com/k8sgpt-ai/k8sgpt/issues/1087)) ([36ccc62](https://github.com/k8sgpt-ai/k8sgpt/commit/36ccc628462ad102712fca115b56f521b2b33b38)) * **deps:** update module github.com/google/generative-ai-go to v0.11.0 ([#1089](https://github.com/k8sgpt-ai/k8sgpt/issues/1089)) ([f30c9f5](https://github.com/k8sgpt-ai/k8sgpt/commit/f30c9f555449bb90bf8242b88b8fae936cb57938)) * **deps:** update module github.com/sashabaranov/go-openai to v1.23.0 ([#1091](https://github.com/k8sgpt-ai/k8sgpt/issues/1091)) ([e74fc08](https://github.com/k8sgpt-ai/k8sgpt/commit/e74fc0838feac5a019a340f7c5ad1c9ae49913fa)) * **deps:** update module golang.org/x/net to v0.25.0 ([#1092](https://github.com/k8sgpt-ai/k8sgpt/issues/1092)) ([fe53907](https://github.com/k8sgpt-ai/k8sgpt/commit/fe53907c44e9cd56b6747f52ae3402bc6ae2bd49)) ### Other * **deps:** pin codecov/codecov-action action to ab904c4 ([#1031](https://github.com/k8sgpt-ai/k8sgpt/issues/1031)) ([e0af76f](https://github.com/k8sgpt-ai/k8sgpt/commit/e0af76f3c9c0120dbc4d9373d69a262e1ec2b7f2)) * **deps:** update actions/checkout digest to 0ad4b8f ([#1078](https://github.com/k8sgpt-ai/k8sgpt/issues/1078)) ([ea8183c](https://github.com/k8sgpt-ai/k8sgpt/commit/ea8183ce848ba58f91cfa68755d6f5b9cf695d36)) * **deps:** update actions/upload-artifact digest to 6546280 ([#1079](https://github.com/k8sgpt-ai/k8sgpt/issues/1079)) ([9b797d7](https://github.com/k8sgpt-ai/k8sgpt/commit/9b797d7e8b4f704dae12acaa7778b6b65e2c36ac)) * **deps:** update amannn/action-semantic-pull-request action to v5.5.2 ([#1088](https://github.com/k8sgpt-ai/k8sgpt/issues/1088)) ([a809a45](https://github.com/k8sgpt-ai/k8sgpt/commit/a809a455f55d1af104ebc0540007aa678581dd21)) * **deps:** update anchore/sbom-action action to v0.15.11 ([#1082](https://github.com/k8sgpt-ai/k8sgpt/issues/1082)) ([12fa5ae](https://github.com/k8sgpt-ai/k8sgpt/commit/12fa5aef4dada597d7059e5717ec7bee3b38c122)) * **deps:** update docker/build-push-action digest to 2cdde99 ([#1032](https://github.com/k8sgpt-ai/k8sgpt/issues/1032)) ([b12c006](https://github.com/k8sgpt-ai/k8sgpt/commit/b12c006c6304165269b90d770048b851e1aa1d1f)) * **deps:** update google-github-actions/release-please-action action to v4.1.0 ([#1045](https://github.com/k8sgpt-ai/k8sgpt/issues/1045)) ([bf6f642](https://github.com/k8sgpt-ai/k8sgpt/commit/bf6f642c280f640f2c9020b325e52670ced2cf50)) ### Docs * add logAnalyzer in README.md ([#1081](https://github.com/k8sgpt-ai/k8sgpt/issues/1081)) ([5cfe332](https://github.com/k8sgpt-ai/k8sgpt/commit/5cfe3325cb556cfb9d0532ae26727441c5177015)) ## [0.3.30](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.29...v0.3.30) (2024-04-26) ### Features * add keda integration ([#1058](https://github.com/k8sgpt-ai/k8sgpt/issues/1058)) ([9a73d19](https://github.com/k8sgpt-ai/k8sgpt/commit/9a73d1923f146aa1343465d89225e64bcb8e0112)) * add minio support ([#1048](https://github.com/k8sgpt-ai/k8sgpt/issues/1048)) ([e6085d4](https://github.com/k8sgpt-ai/k8sgpt/commit/e6085d4191a1695e295f4f6a2ac7219b67a37225)) * add Resource Kind in output ([#1069](https://github.com/k8sgpt-ai/k8sgpt/issues/1069)) ([aa276a5](https://github.com/k8sgpt-ai/k8sgpt/commit/aa276a5379b3d24a8e7a1f8b1193832df5a46220)) ### Bug Fixes * **deps:** update k8s.io/utils digest to 4693a02 ([#1037](https://github.com/k8sgpt-ai/k8sgpt/issues/1037)) ([94cdce4](https://github.com/k8sgpt-ai/k8sgpt/commit/94cdce44b49e0bb85e8b541688b2206e7c1dc33d)) * **deps:** update module cloud.google.com/go/storage to v1.39.1 ([#1029](https://github.com/k8sgpt-ai/k8sgpt/issues/1029)) ([a3896f4](https://github.com/k8sgpt-ai/k8sgpt/commit/a3896f4518ec6666a43de22a24a18f2b93c58073)) * **deps:** update module cloud.google.com/go/storage to v1.40.0 ([#1054](https://github.com/k8sgpt-ai/k8sgpt/issues/1054)) ([6df0169](https://github.com/k8sgpt-ai/k8sgpt/commit/6df01694916504cc4af3795361a4285098e2de85)) * **deps:** update module github.com/aws/aws-sdk-go to v1.51.14 ([#1051](https://github.com/k8sgpt-ai/k8sgpt/issues/1051)) ([007b4bb](https://github.com/k8sgpt-ai/k8sgpt/commit/007b4bb8ec4b36705f76fd2f5d96464c75915573)) * **deps:** update module github.com/aws/aws-sdk-go to v1.51.21 ([#1056](https://github.com/k8sgpt-ai/k8sgpt/issues/1056)) ([ccb692c](https://github.com/k8sgpt-ai/k8sgpt/commit/ccb692c1fdc5496d9d5810dfe41dbf1bdeb68d00)) * **deps:** update module github.com/aws/aws-sdk-go to v1.51.8 ([#1046](https://github.com/k8sgpt-ai/k8sgpt/issues/1046)) ([19ae31b](https://github.com/k8sgpt-ai/k8sgpt/commit/19ae31b5dd5c54413025cee8081d112223e38400)) * **deps:** update module github.com/google/generative-ai-go to v0.10.0 ([#1047](https://github.com/k8sgpt-ai/k8sgpt/issues/1047)) ([6b38a56](https://github.com/k8sgpt-ai/k8sgpt/commit/6b38a56afbdaa8e0d8f025088a52d3022673ef9d)) * **deps:** update module github.com/sashabaranov/go-openai to v1.20.4 ([#1039](https://github.com/k8sgpt-ai/k8sgpt/issues/1039)) ([6a46a26](https://github.com/k8sgpt-ai/k8sgpt/commit/6a46a26789f730d298cf49a706421f36bc8523b1)) * **deps:** update module golang.org/x/net to v0.23.0 [security] ([#1071](https://github.com/k8sgpt-ai/k8sgpt/issues/1071)) ([693b23f](https://github.com/k8sgpt-ai/k8sgpt/commit/693b23f1fc33659a3c4f52fc4d9c23348b22bfb1)) * invalid ParentObj in output ([#1068](https://github.com/k8sgpt-ai/k8sgpt/issues/1068)) ([b2ab943](https://github.com/k8sgpt-ai/k8sgpt/commit/b2ab94375e4233cdfa9762877995445c313bb962)) * remove show password in auth list ([#1061](https://github.com/k8sgpt-ai/k8sgpt/issues/1061)) ([9e02637](https://github.com/k8sgpt-ai/k8sgpt/commit/9e0263778f6dbc179184fa9d86f07d808283d63e)) * set topP from config ([#1053](https://github.com/k8sgpt-ai/k8sgpt/issues/1053)) ([c162cc2](https://github.com/k8sgpt-ai/k8sgpt/commit/c162cc22ee468070e0602d3fd684b022fa585c4f)) ### Other * **deps:** update anchore/sbom-action action to v0.15.10 ([#1044](https://github.com/k8sgpt-ai/k8sgpt/issues/1044)) ([e05a902](https://github.com/k8sgpt-ai/k8sgpt/commit/e05a902d904fc0b63998ae290f15e79d330317fb)) * **deps:** update cohere client implementation to v2 ([#1062](https://github.com/k8sgpt-ai/k8sgpt/issues/1062)) ([eb7687a](https://github.com/k8sgpt-ai/k8sgpt/commit/eb7687a08917ad4048c6f00c17bb45591a935a3a)) * **deps:** update docker/login-action digest to e92390c ([#1033](https://github.com/k8sgpt-ai/k8sgpt/issues/1033)) ([c872e49](https://github.com/k8sgpt-ai/k8sgpt/commit/c872e495ad6f787cf566a5b2f295deb3f08aba15)) * **deps:** update docker/setup-buildx-action digest to 2b51285 ([#1036](https://github.com/k8sgpt-ai/k8sgpt/issues/1036)) ([10c00ba](https://github.com/k8sgpt-ai/k8sgpt/commit/10c00ba9fe61a3ee1dc90d87dd7997da276905b4)) * **deps:** update docker/setup-buildx-action digest to d70bba7 ([#1066](https://github.com/k8sgpt-ai/k8sgpt/issues/1066)) ([3eaf776](https://github.com/k8sgpt-ai/k8sgpt/commit/3eaf776249719a0a13909d24e6b48deb6bf818b6)) * update license file path to avoid conflicting installations ([#878](https://github.com/k8sgpt-ai/k8sgpt/issues/878)) ([#1073](https://github.com/k8sgpt-ai/k8sgpt/issues/1073)) ([85a76a3](https://github.com/k8sgpt-ai/k8sgpt/commit/85a76a3be06df0ff713192d1f08fd01d1e8f219b)) * update renovate config and bundle deps in groups ([#1026](https://github.com/k8sgpt-ai/k8sgpt/issues/1026)) ([bd2e06b](https://github.com/k8sgpt-ai/k8sgpt/commit/bd2e06bae72528c5af1b4f44674d624d474d40dc)) ### Refactoring * replace util.SliceContainsString with slices.Contains & make fmt ([#1041](https://github.com/k8sgpt-ai/k8sgpt/issues/1041)) ([1ae4e75](https://github.com/k8sgpt-ai/k8sgpt/commit/1ae4e751967850e8146f8f3fa04c0dd302ef15bf)) ## [0.3.29](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.28...v0.3.29) (2024-03-22) ### Features * codecov ([#1023](https://github.com/k8sgpt-ai/k8sgpt/issues/1023)) ([fe81d16](https://github.com/k8sgpt-ai/k8sgpt/commit/fe81d16f756e5ea9db909e42e6caf1e17e040f86)) ### Other * allows an environmental override of the default AWS region and… ([#1025](https://github.com/k8sgpt-ai/k8sgpt/issues/1025)) ([8f8f5c6](https://github.com/k8sgpt-ai/k8sgpt/commit/8f8f5c6df7fbcd08ee48d91a4f2e011a3e69e4ac)) ## [0.3.28](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.27...v0.3.28) (2024-03-14) ### Features * add Google Vertex AI as provider to utilize gemini via GCP ([#984](https://github.com/k8sgpt-ai/k8sgpt/issues/984)) ([55ac0b2](https://github.com/k8sgpt-ai/k8sgpt/commit/55ac0b2129a438661a0253251f546db6b59f2b92)) * add proxysettings for azureopenai and openai ([#987](https://github.com/k8sgpt-ai/k8sgpt/issues/987)) ([307710e](https://github.com/k8sgpt-ai/k8sgpt/commit/307710eddc1c3f96f40a674f7dda786510e9c4cc)) * aws integration ([#967](https://github.com/k8sgpt-ai/k8sgpt/issues/967)) ([a81377f](https://github.com/k8sgpt-ai/k8sgpt/commit/a81377f72db7f322e0afbb6d613c2bfffecf8080)) * enable Rest api using grpc-gateway ([#834](https://github.com/k8sgpt-ai/k8sgpt/issues/834)) ([f2138c7](https://github.com/k8sgpt-ai/k8sgpt/commit/f2138c71017b391625eebdfb4c5708c824824f69)) ### Bug Fixes * analyze command default backend bug ([#966](https://github.com/k8sgpt-ai/k8sgpt/issues/966)) ([aab8d77](https://github.com/k8sgpt-ai/k8sgpt/commit/aab8d77febdd4b42ff74aafbb2ada27745c04ae1)) * **deps:** update module cloud.google.com/go/storage to v1.38.0 ([#950](https://github.com/k8sgpt-ai/k8sgpt/issues/950)) ([6207c70](https://github.com/k8sgpt-ai/k8sgpt/commit/6207c70c51d2885c4590c255c8f78e7ee2009034)) * **deps:** update module github.com/aws/aws-sdk-go to v1.50.20 ([#930](https://github.com/k8sgpt-ai/k8sgpt/issues/930)) ([3f0356b](https://github.com/k8sgpt-ai/k8sgpt/commit/3f0356be662c32d82ce4f3db05f859477823717d)) * **deps:** update module github.com/aws/aws-sdk-go to v1.50.21 ([#970](https://github.com/k8sgpt-ai/k8sgpt/issues/970)) ([00c91f0](https://github.com/k8sgpt-ai/k8sgpt/commit/00c91f05a62b2c8b2d756b58b95279195ff38d3d)) * **deps:** update module github.com/aws/aws-sdk-go to v1.50.22 ([#971](https://github.com/k8sgpt-ai/k8sgpt/issues/971)) ([6ac815c](https://github.com/k8sgpt-ai/k8sgpt/commit/6ac815c10fb073f4251e338ab22e247625f21406)) * **deps:** update module github.com/aws/aws-sdk-go to v1.50.34 ([#974](https://github.com/k8sgpt-ai/k8sgpt/issues/974)) ([425f33b](https://github.com/k8sgpt-ai/k8sgpt/commit/425f33bb2ddf8cdaff079b097d6956f675c89b0e)) * **deps:** update module github.com/azure/azure-sdk-for-go/sdk/storage/azblob to v1.3.1 ([#992](https://github.com/k8sgpt-ai/k8sgpt/issues/992)) ([85f18dd](https://github.com/k8sgpt-ai/k8sgpt/commit/85f18dde1f820fe2413cc6b3109e67b7a010142c)) * **deps:** update module github.com/google/generative-ai-go to v0.8.0 ([#965](https://github.com/k8sgpt-ai/k8sgpt/issues/965)) ([248260e](https://github.com/k8sgpt-ai/k8sgpt/commit/248260e081327de9f9d1d2c851efab2b4a3e7ede)) * **deps:** update module github.com/prometheus/client_golang to v1.19.0 ([#989](https://github.com/k8sgpt-ai/k8sgpt/issues/989)) ([4065fae](https://github.com/k8sgpt-ai/k8sgpt/commit/4065faef13691f9cf1f50696c62d3b30b0933b4b)) * **deps:** update module github.com/sashabaranov/go-openai to v1.19.4 ([#963](https://github.com/k8sgpt-ai/k8sgpt/issues/963)) ([8b0b61e](https://github.com/k8sgpt-ai/k8sgpt/commit/8b0b61e596f790b9558a5e3d1f634a5ee1c6cb0c)) * **deps:** update module github.com/sashabaranov/go-openai to v1.20.0 ([#977](https://github.com/k8sgpt-ai/k8sgpt/issues/977)) ([e07822c](https://github.com/k8sgpt-ai/k8sgpt/commit/e07822c10bff5dbd91f4da592914c25538353d6b)) * **deps:** update module github.com/sashabaranov/go-openai to v1.20.1 ([#986](https://github.com/k8sgpt-ai/k8sgpt/issues/986)) ([88a7907](https://github.com/k8sgpt-ai/k8sgpt/commit/88a7907db4700c241e9aa109bc3d8604a8186f87)) * **deps:** update module github.com/sashabaranov/go-openai to v1.20.2 ([#991](https://github.com/k8sgpt-ai/k8sgpt/issues/991)) ([d2754d3](https://github.com/k8sgpt-ai/k8sgpt/commit/d2754d320fb1f285f93fdced2b8469280bd47fd2)) * **deps:** update module github.com/schollz/progressbar/v3 to v3.14.2 ([#983](https://github.com/k8sgpt-ai/k8sgpt/issues/983)) ([af3732a](https://github.com/k8sgpt-ai/k8sgpt/commit/af3732ad067b809c54c5f08f6cf5a7a519b452d7)) * **deps:** update module github.com/stretchr/testify to v1.9.0 ([#999](https://github.com/k8sgpt-ai/k8sgpt/issues/999)) ([1491e67](https://github.com/k8sgpt-ai/k8sgpt/commit/1491e675673dcc13ccf6ac1778113762542e8cbc)) * **deps:** update module go.uber.org/zap to v1.27.0 ([#972](https://github.com/k8sgpt-ai/k8sgpt/issues/972)) ([8f00218](https://github.com/k8sgpt-ai/k8sgpt/commit/8f002180901c8bf7e6b1a5451dd97ef566260b0f)) * **deps:** update module google.golang.org/api to v0.165.0 ([#959](https://github.com/k8sgpt-ai/k8sgpt/issues/959)) ([cc99bd5](https://github.com/k8sgpt-ai/k8sgpt/commit/cc99bd51f05db4e87f806ac58ee1cb7a83b25e4d)) * **deps:** update module google.golang.org/api to v0.167.0 ([#973](https://github.com/k8sgpt-ai/k8sgpt/issues/973)) ([6103c96](https://github.com/k8sgpt-ai/k8sgpt/commit/6103c96c41e10e2fe13d285ff15a36bf2fbeb5c2)) * **deps:** update module google.golang.org/grpc to v1.62.0 ([#975](https://github.com/k8sgpt-ai/k8sgpt/issues/975)) ([97446aa](https://github.com/k8sgpt-ai/k8sgpt/commit/97446aae079824d6556416314c0a27514088a667)) * **deps:** update module gopkg.in/yaml.v2 to v3 ([#957](https://github.com/k8sgpt-ai/k8sgpt/issues/957)) ([f929e7f](https://github.com/k8sgpt-ai/k8sgpt/commit/f929e7feea5931ddec77af49dd08937aca85fd49)) * **deps:** update module gopkg.in/yaml.v2 to v3 ([#979](https://github.com/k8sgpt-ai/k8sgpt/issues/979)) ([35f5185](https://github.com/k8sgpt-ai/k8sgpt/commit/35f51859140c78ce953443afcc27f77230287809)) * **deps:** update module gopkg.in/yaml.v2 to v3 ([#980](https://github.com/k8sgpt-ai/k8sgpt/issues/980)) ([334a86a](https://github.com/k8sgpt-ai/k8sgpt/commit/334a86aaf40e5421929cf380191841db064d9bf7)) * log analyzer failed with multiple containers in the pod ([#920](https://github.com/k8sgpt-ai/k8sgpt/issues/920)) ([98286a9](https://github.com/k8sgpt-ai/k8sgpt/commit/98286a965e4c4c680deeb43d3397b51089968366)) * set result name and namespace to trivy vulnreport and configaudi… ([#869](https://github.com/k8sgpt-ai/k8sgpt/issues/869)) ([a3cd7e6](https://github.com/k8sgpt-ai/k8sgpt/commit/a3cd7e6385365a1d190a9e8439311cb9d5eeda56)) * shorthand for the http flag in serve command ([#969](https://github.com/k8sgpt-ai/k8sgpt/issues/969)) ([f55f837](https://github.com/k8sgpt-ai/k8sgpt/commit/f55f8370ebf0db6db629641337cd78ad7f120865)) ### Other * attempt to group renovate deps ([#1007](https://github.com/k8sgpt-ai/k8sgpt/issues/1007)) ([adf4f17](https://github.com/k8sgpt-ai/k8sgpt/commit/adf4f17085672fd5ae78dad4f8ac1d887029836d)) * **deps:** update anchore/sbom-action action to v0.15.9 ([#1004](https://github.com/k8sgpt-ai/k8sgpt/issues/1004)) ([b05b6a3](https://github.com/k8sgpt-ai/k8sgpt/commit/b05b6a38ed4a9fc017f9dcb52cff8a332c11056d)) * **deps:** update docker/build-push-action digest to af5a7ed ([#1003](https://github.com/k8sgpt-ai/k8sgpt/issues/1003)) ([b58b719](https://github.com/k8sgpt-ai/k8sgpt/commit/b58b7191af2fe082d94d46ef6a2784c1ea322340)) * **deps:** update docker/setup-buildx-action digest to 0d103c3 ([#988](https://github.com/k8sgpt-ai/k8sgpt/issues/988)) ([f24bcd8](https://github.com/k8sgpt-ai/k8sgpt/commit/f24bcd88b6a915798897b49a562b86265a9b524c)) * **deps:** update reviewdog/action-golangci-lint digest to 00311c2 ([#1002](https://github.com/k8sgpt-ai/k8sgpt/issues/1002)) ([4ec143a](https://github.com/k8sgpt-ai/k8sgpt/commit/4ec143ab772ca4dc3072c248e95da8f7c0a2974b)) ## [0.3.27](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.26...v0.3.27) (2024-02-15) ### Features * add huggingface provider ([#893](https://github.com/k8sgpt-ai/k8sgpt/issues/893)) ([2fd476e](https://github.com/k8sgpt-ai/k8sgpt/commit/2fd476e12624e30570c0819594f2668f720381d6)) * added FailedMount event reason to get the failure ([#883](https://github.com/k8sgpt-ai/k8sgpt/issues/883)) ([78126b2](https://github.com/k8sgpt-ai/k8sgpt/commit/78126b2328c1b3f81a269d203e86128104050010)) * enables remote custom analyzers ([#906](https://github.com/k8sgpt-ai/k8sgpt/issues/906)) ([c8c9dbf](https://github.com/k8sgpt-ai/k8sgpt/commit/c8c9dbfadc72a193ab9f3431d02d50ac5ab5d071)) ### Bug Fixes * **deps:** update k8s.io/utils digest to e7106e6 ([#897](https://github.com/k8sgpt-ai/k8sgpt/issues/897)) ([28c4c57](https://github.com/k8sgpt-ai/k8sgpt/commit/28c4c57e4566b9b888a5633090ccb70875d30106)) * **deps:** update module buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go to v1.3.0-20240128172516-6bf6a55ff115.2 ([#899](https://github.com/k8sgpt-ai/k8sgpt/issues/899)) ([e3eee6d](https://github.com/k8sgpt-ai/k8sgpt/commit/e3eee6d9566a59fd62e6bb804257b1383f75e3ef)) * **deps:** update module cloud.google.com/go/storage to v1.37.0 ([#934](https://github.com/k8sgpt-ai/k8sgpt/issues/934)) ([3d2554b](https://github.com/k8sgpt-ai/k8sgpt/commit/3d2554b9cd8817b24cf8858a107420d6d8424aa4)) * **deps:** update module github.com/aws/aws-sdk-go to v1.49.21 ([#868](https://github.com/k8sgpt-ai/k8sgpt/issues/868)) ([88002e7](https://github.com/k8sgpt-ai/k8sgpt/commit/88002e7e8c3e9c71365c44e136a6f1a8d35e1744)) * **deps:** update module github.com/aws/aws-sdk-go to v1.50.2 ([#887](https://github.com/k8sgpt-ai/k8sgpt/issues/887)) ([817d9cf](https://github.com/k8sgpt-ai/k8sgpt/commit/817d9cf754d307d374befc0d57919eb7a0183aaf)) * **deps:** update module github.com/azure/azure-sdk-for-go/sdk/azidentity to v1.5.1 ([#939](https://github.com/k8sgpt-ai/k8sgpt/issues/939)) ([ce7c955](https://github.com/k8sgpt-ai/k8sgpt/commit/ce7c9551bcb1a8b24922a1eb062605bbfeec7929)) * **deps:** update module github.com/azure/azure-sdk-for-go/sdk/storage/azblob to v1.3.0 ([#952](https://github.com/k8sgpt-ai/k8sgpt/issues/952)) ([fea2ed1](https://github.com/k8sgpt-ai/k8sgpt/commit/fea2ed1fff5fb5a46d6abc2feb72e1e1adf3b69b)) * **deps:** update module github.com/google/generative-ai-go to v0.7.0 ([#940](https://github.com/k8sgpt-ai/k8sgpt/issues/940)) ([3c8d9d4](https://github.com/k8sgpt-ai/k8sgpt/commit/3c8d9d42e573f27185a1572d1bc06f8af87f3a0b)) * **deps:** update module github.com/prometheus/prometheus to v2 ([#863](https://github.com/k8sgpt-ai/k8sgpt/issues/863)) ([a253af2](https://github.com/k8sgpt-ai/k8sgpt/commit/a253af23b601b23179be5019fbb832a41423cdae)) * **deps:** update module github.com/pterm/pterm to v0.12.75 ([#881](https://github.com/k8sgpt-ai/k8sgpt/issues/881)) ([e7d690a](https://github.com/k8sgpt-ai/k8sgpt/commit/e7d690afd12cb71d7b344ba92bf059ae18a993c8)) * **deps:** update module github.com/pterm/pterm to v0.12.78 ([#890](https://github.com/k8sgpt-ai/k8sgpt/issues/890)) ([f9c1b90](https://github.com/k8sgpt-ai/k8sgpt/commit/f9c1b903385978be56f9c4bc87089bd1c761bbea)) * **deps:** update module github.com/pterm/pterm to v0.12.79 ([#943](https://github.com/k8sgpt-ai/k8sgpt/issues/943)) ([bfbb5c7](https://github.com/k8sgpt-ai/k8sgpt/commit/bfbb5c7e03cad144f6037c7233ffc0817fd403e4)) * **deps:** update module github.com/sashabaranov/go-openai to v1.18.1 ([#871](https://github.com/k8sgpt-ai/k8sgpt/issues/871)) ([6c62c1a](https://github.com/k8sgpt-ai/k8sgpt/commit/6c62c1a0fcd38cf9de8a99cda6f37b221740b9c8)) * **deps:** update module github.com/sashabaranov/go-openai to v1.18.2 ([#874](https://github.com/k8sgpt-ai/k8sgpt/issues/874)) ([4de1bbd](https://github.com/k8sgpt-ai/k8sgpt/commit/4de1bbd6f72ca83d46ce5955bac50dffc99af03d)) * **deps:** update module github.com/sashabaranov/go-openai to v1.19.2 ([#886](https://github.com/k8sgpt-ai/k8sgpt/issues/886)) ([c601972](https://github.com/k8sgpt-ai/k8sgpt/commit/c6019728aea837884620e0b4894568802a948a6e)) * **deps:** update module github.com/sashabaranov/go-openai to v1.19.3 ([#937](https://github.com/k8sgpt-ai/k8sgpt/issues/937)) ([f2eb1ef](https://github.com/k8sgpt-ai/k8sgpt/commit/f2eb1ef5334877fd3a26dda8c92023f831ea857e)) * **deps:** update module golang.org/x/term to v0.17.0 ([#941](https://github.com/k8sgpt-ai/k8sgpt/issues/941)) ([4e57088](https://github.com/k8sgpt-ai/k8sgpt/commit/4e57088a0137767a42c778a59ff07fff04c04289)) * **deps:** update module google.golang.org/api to v0.157.0 ([#860](https://github.com/k8sgpt-ai/k8sgpt/issues/860)) ([72e08ef](https://github.com/k8sgpt-ai/k8sgpt/commit/72e08efff1fc501dfcba791c9d940e575f3e2395)) * **deps:** update module google.golang.org/api to v0.164.0 ([#953](https://github.com/k8sgpt-ai/k8sgpt/issues/953)) ([29b482f](https://github.com/k8sgpt-ai/k8sgpt/commit/29b482f5978795fa8db729030bd75803e2e61f95)) * **deps:** update module google.golang.org/grpc to v1.61.1 ([#954](https://github.com/k8sgpt-ai/k8sgpt/issues/954)) ([9c1f1b8](https://github.com/k8sgpt-ai/k8sgpt/commit/9c1f1b8804a26f549379efe637d0bedb8e2cb890)) * **deps:** update module gopkg.in/yaml.v2 to v3 ([#866](https://github.com/k8sgpt-ai/k8sgpt/issues/866)) ([81d6604](https://github.com/k8sgpt-ai/k8sgpt/commit/81d660447d236cd03b75866871bb69f2c77c5c66)) * **deps:** update module gopkg.in/yaml.v2 to v3 ([#875](https://github.com/k8sgpt-ai/k8sgpt/issues/875)) ([1f371e2](https://github.com/k8sgpt-ai/k8sgpt/commit/1f371e2807c47dbb4613bf873ec67a77e8e6c80c)) * **deps:** update module gopkg.in/yaml.v2 to v3 ([#956](https://github.com/k8sgpt-ai/k8sgpt/issues/956)) ([d9fe744](https://github.com/k8sgpt-ai/k8sgpt/commit/d9fe7446af428209610adc83ec17cf50491a5a47)) * lint errors ([#923](https://github.com/k8sgpt-ai/k8sgpt/issues/923)) ([3415031](https://github.com/k8sgpt-ai/k8sgpt/commit/3415031006bb5899019e68d33ac6083d03ef864b)) * typo in httproute files name ([#877](https://github.com/k8sgpt-ai/k8sgpt/issues/877)) ([cdbeb14](https://github.com/k8sgpt-ai/k8sgpt/commit/cdbeb146a28ebc21ac2c4d27e977b1771f9290b4)) * unused variable failure warning in webhooks file ([#916](https://github.com/k8sgpt-ai/k8sgpt/issues/916)) ([3f0964a](https://github.com/k8sgpt-ai/k8sgpt/commit/3f0964ad385390f53516904219fbfc47b989d31f)) ### Other * **deps:** update actions/upload-artifact digest to 26f96df ([#888](https://github.com/k8sgpt-ai/k8sgpt/issues/888)) ([483a9da](https://github.com/k8sgpt-ai/k8sgpt/commit/483a9dad103ad1af82491dc1d5e0a39bb4865a1b)) * **deps:** update actions/upload-artifact digest to 5d5d22a ([#925](https://github.com/k8sgpt-ai/k8sgpt/issues/925)) ([070aa7f](https://github.com/k8sgpt-ai/k8sgpt/commit/070aa7fdd0982c0c7f02a1da9e6797d5efaa5586)) * **deps:** update actions/upload-artifact digest to 694cdab ([#880](https://github.com/k8sgpt-ai/k8sgpt/issues/880)) ([3cf18e7](https://github.com/k8sgpt-ai/k8sgpt/commit/3cf18e783edb341b7bdd6aa20dbcce11971fa241)) * **deps:** update anchore/sbom-action action to v0.15.4 ([#879](https://github.com/k8sgpt-ai/k8sgpt/issues/879)) ([d213399](https://github.com/k8sgpt-ai/k8sgpt/commit/d2133991617697b13b8846f2acb3a3bb6cebb160)) * **deps:** update anchore/sbom-action action to v0.15.5 ([#885](https://github.com/k8sgpt-ai/k8sgpt/issues/885)) ([60853fe](https://github.com/k8sgpt-ai/k8sgpt/commit/60853fe4eb8de7a1fdbaea388c3d2d6205e273a6)) * **deps:** update anchore/sbom-action action to v0.15.8 ([#926](https://github.com/k8sgpt-ai/k8sgpt/issues/926)) ([f61c3e2](https://github.com/k8sgpt-ai/k8sgpt/commit/f61c3e228c69fa160735ddb2c1347720112b738f)) * **deps:** update golang docker tag to v1.22 ([#931](https://github.com/k8sgpt-ai/k8sgpt/issues/931)) ([37228d8](https://github.com/k8sgpt-ai/k8sgpt/commit/37228d88e357c66c5574559ae27a52fdf28418b8)) * **deps:** update reviewdog/action-golangci-lint digest to 8e1117c ([#915](https://github.com/k8sgpt-ai/k8sgpt/issues/915)) ([599be33](https://github.com/k8sgpt-ai/k8sgpt/commit/599be33f38ad1fd688b8e7824102a7944d516435)) * **deps:** update reviewdog/action-golangci-lint digest to f016e79 ([#714](https://github.com/k8sgpt-ai/k8sgpt/issues/714)) ([335616c](https://github.com/k8sgpt-ai/k8sgpt/commit/335616c20f7f8d9fefab4976d986a8d3b4867111)) * grpc update ([#938](https://github.com/k8sgpt-ai/k8sgpt/issues/938)) ([bbf61f5](https://github.com/k8sgpt-ai/k8sgpt/commit/bbf61f53d4fb9244b5a79ae953370296ca9fd44b)) * improve codebase and doc quality ([#922](https://github.com/k8sgpt-ai/k8sgpt/issues/922)) ([d97dea2](https://github.com/k8sgpt-ai/k8sgpt/commit/d97dea289681cd061ca0796208c50720bdb08914)) * linting improvements and catching false positives ([#882](https://github.com/k8sgpt-ai/k8sgpt/issues/882)) ([2effbb3](https://github.com/k8sgpt-ai/k8sgpt/commit/2effbb345ad1c2771ec798e06ccde68d3253b4bc)) * set correct license during package build ([#872](https://github.com/k8sgpt-ai/k8sgpt/issues/872)) ([42be51b](https://github.com/k8sgpt-ai/k8sgpt/commit/42be51bc8f625a35b1435c461d9a32c3c4905f1c)) * updated deps ([#951](https://github.com/k8sgpt-ai/k8sgpt/issues/951)) ([015bccf](https://github.com/k8sgpt-ai/k8sgpt/commit/015bccfc2eae587e0ade371211404f5af4c37d27)) ## [0.3.26](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.25...v0.3.26) (2024-01-14) ### Features * initial Prometheus analyzers ([#855](https://github.com/k8sgpt-ai/k8sgpt/issues/855)) ([45fa827](https://github.com/k8sgpt-ai/k8sgpt/commit/45fa827c046b91d901a08bec1a892d9c0917f350)) * interactive mode ([#854](https://github.com/k8sgpt-ai/k8sgpt/issues/854)) ([9da75e0](https://github.com/k8sgpt-ai/k8sgpt/commit/9da75e02bc17146898377e4f90b7f59c5a8e0eee)) * unify aiClientName const for all providers ([#848](https://github.com/k8sgpt-ai/k8sgpt/issues/848)) ([5c17c24](https://github.com/k8sgpt-ai/k8sgpt/commit/5c17c240550609d9fb7771fe67fe1ab19660b4da)) ### Bug Fixes * **deps:** update module github.com/aws/aws-sdk-go to v1.49.16 ([#847](https://github.com/k8sgpt-ai/k8sgpt/issues/847)) ([ce4910b](https://github.com/k8sgpt-ai/k8sgpt/commit/ce4910bc5d064f80076877d7a096fff903308b63)) * **deps:** update module github.com/aws/aws-sdk-go to v1.49.17 ([#852](https://github.com/k8sgpt-ai/k8sgpt/issues/852)) ([85ebd12](https://github.com/k8sgpt-ai/k8sgpt/commit/85ebd12c30d369c5ef9a42b5a834d091523a7b6e)) * **deps:** update module github.com/aws/aws-sdk-go to v1.49.18 ([#856](https://github.com/k8sgpt-ai/k8sgpt/issues/856)) ([4106d39](https://github.com/k8sgpt-ai/k8sgpt/commit/4106d39c322940413ebfd9ac0bf6f5bd31830e93)) * **deps:** update module github.com/aws/aws-sdk-go to v1.49.19 ([#859](https://github.com/k8sgpt-ai/k8sgpt/issues/859)) ([6a2f315](https://github.com/k8sgpt-ai/k8sgpt/commit/6a2f315b2f4344f2924b7915e8a1393f9732a1e9)) * **deps:** update module github.com/sashabaranov/go-openai to v1.17.11 ([#853](https://github.com/k8sgpt-ai/k8sgpt/issues/853)) ([1979c86](https://github.com/k8sgpt-ai/k8sgpt/commit/1979c86d0f59921d55cd4229a37d604a6f1dc578)) * **deps:** update module github.com/sashabaranov/go-openai to v1.17.11 ([#861](https://github.com/k8sgpt-ai/k8sgpt/issues/861)) ([40b5b7e](https://github.com/k8sgpt-ai/k8sgpt/commit/40b5b7e185c8d335bdefb131988b9900ad26bac3)) * **deps:** update module gopkg.in/yaml.v2 to v3 ([#864](https://github.com/k8sgpt-ai/k8sgpt/issues/864)) ([36ba6c5](https://github.com/k8sgpt-ai/k8sgpt/commit/36ba6c5147a9ed75c14dbba4bc06cae903e651a4)) * **deps:** update module gopkg.in/yaml.v2 to v3 ([#865](https://github.com/k8sgpt-ai/k8sgpt/issues/865)) ([c55025d](https://github.com/k8sgpt-ai/k8sgpt/commit/c55025d04ebf9da0f6092aabb0b043ccef05164c)) ### Other * **deps:** update actions/upload-artifact digest to 1eb3cb2 ([#867](https://github.com/k8sgpt-ai/k8sgpt/issues/867)) ([4ce56f3](https://github.com/k8sgpt-ai/k8sgpt/commit/4ce56f38b4338a6a2fe69f588b0f17e0b54d0ae6)) * **deps:** update anchore/sbom-action action to v0.15.3 ([#850](https://github.com/k8sgpt-ai/k8sgpt/issues/850)) ([12f764d](https://github.com/k8sgpt-ai/k8sgpt/commit/12f764d5846accbd987d40f69a153dceb9954f39)) ### Docs * adjusted README information about providers ([#844](https://github.com/k8sgpt-ai/k8sgpt/issues/844)) ([745e960](https://github.com/k8sgpt-ai/k8sgpt/commit/745e960f492e6dd0e50aa4a1ce7239c677025024)) ## [0.3.25](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.24...v0.3.25) (2024-01-05) ### Features * added Google GenAI client; simplified IAI/clients API surface. ([#829](https://github.com/k8sgpt-ai/k8sgpt/issues/829)) ([e7d4149](https://github.com/k8sgpt-ai/k8sgpt/commit/e7d41496ddaa145c70079852da8b2ce3b3b7289f)) * code_cov badge ([#821](https://github.com/k8sgpt-ai/k8sgpt/issues/821)) ([fcd29a5](https://github.com/k8sgpt-ai/k8sgpt/commit/fcd29a547d73ba48935762e2f568f5755f5c6ed3)) * coverage reports ([#819](https://github.com/k8sgpt-ai/k8sgpt/issues/819)) ([3d0ba3e](https://github.com/k8sgpt-ai/k8sgpt/commit/3d0ba3e78cabaf5f1262c5b5b16ebabad974fa87)) ### Bug Fixes * **deps:** update module github.com/aws/aws-sdk-go to v1.49.10 ([#811](https://github.com/k8sgpt-ai/k8sgpt/issues/811)) ([e5cc4a2](https://github.com/k8sgpt-ai/k8sgpt/commit/e5cc4a28cb3682e7094e6ceddf91b65da991ddb6)) * **deps:** update module github.com/aws/aws-sdk-go to v1.49.12 ([#813](https://github.com/k8sgpt-ai/k8sgpt/issues/813)) ([91613ba](https://github.com/k8sgpt-ai/k8sgpt/commit/91613baa5cc5244c93deb344abcdd905802eef30)) * **deps:** update module github.com/aws/aws-sdk-go to v1.49.14 ([#822](https://github.com/k8sgpt-ai/k8sgpt/issues/822)) ([526e22f](https://github.com/k8sgpt-ai/k8sgpt/commit/526e22f88b8de15eceb10965b045ef0366ff2d6c)) * **deps:** update module github.com/aws/aws-sdk-go to v1.49.15 ([#835](https://github.com/k8sgpt-ai/k8sgpt/issues/835)) ([e78ff05](https://github.com/k8sgpt-ai/k8sgpt/commit/e78ff054190cd54cabe17d77ac69443e517f1e55)) * **deps:** update module github.com/prometheus/client_golang to v1.18.0 ([#814](https://github.com/k8sgpt-ai/k8sgpt/issues/814)) ([6eb8f67](https://github.com/k8sgpt-ai/k8sgpt/commit/6eb8f6793ed989ba3ac7ed00336345f68b09bf45)) * **deps:** update module github.com/sashabaranov/go-openai to v1.17.10 ([#824](https://github.com/k8sgpt-ai/k8sgpt/issues/824)) ([4314804](https://github.com/k8sgpt-ai/k8sgpt/commit/4314804ca7e782f5149dc2078ba9c859edc4688a)) * **deps:** update module golang.org/x/term to v0.16.0 ([#831](https://github.com/k8sgpt-ai/k8sgpt/issues/831)) ([4de989c](https://github.com/k8sgpt-ai/k8sgpt/commit/4de989c803ee43a02d75112d1b3a54daee3dd9af)) * **deps:** update module google.golang.org/api to v0.155.0 ([#836](https://github.com/k8sgpt-ai/k8sgpt/issues/836)) ([105a239](https://github.com/k8sgpt-ai/k8sgpt/commit/105a239d94384f4096c01d9978564040773ab56e)) * no explain case, improved readability. ([#825](https://github.com/k8sgpt-ai/k8sgpt/issues/825)) ([035348d](https://github.com/k8sgpt-ai/k8sgpt/commit/035348d8a0d290ac26b42425945eaafe038cedc5)) ### Other * added basic server startup test ([#817](https://github.com/k8sgpt-ai/k8sgpt/issues/817)) ([3e7cea7](https://github.com/k8sgpt-ai/k8sgpt/commit/3e7cea7bd39253718bc3d2f8b10ac5fc9b98cbc2)) * **deps:** pin codecov/codecov-action action to eaaf4be ([#820](https://github.com/k8sgpt-ai/k8sgpt/issues/820)) ([2f0f2df](https://github.com/k8sgpt-ai/k8sgpt/commit/2f0f2dfa8a5957cb8b10864c14d7883158723a6a)) * **deps:** update anchore/sbom-action action to v0.15.2 ([#823](https://github.com/k8sgpt-ai/k8sgpt/issues/823)) ([70c6892](https://github.com/k8sgpt-ai/k8sgpt/commit/70c68929d8d963c0bd17390c76e366d4339f56b9)) * lint fixes ([#833](https://github.com/k8sgpt-ai/k8sgpt/issues/833)) ([a7e9b48](https://github.com/k8sgpt-ai/k8sgpt/commit/a7e9b486bad7c2d62878e470a755d1fef3803680)) * remove code cov ([#832](https://github.com/k8sgpt-ai/k8sgpt/issues/832)) ([a774265](https://github.com/k8sgpt-ai/k8sgpt/commit/a77426593d7f3a8cfa810336ff08a2266db7fb4f)) ### Dependency Updates * go module bump to fix CVE: GHSA-45x7-px36-x8w8 & GHSA-7ww5-4wqc-m92c ([#810](https://github.com/k8sgpt-ai/k8sgpt/issues/810)) ([b17fd7c](https://github.com/k8sgpt-ai/k8sgpt/commit/b17fd7c98644afa70d414fcb32e49e61e1c831ad)) ## [0.3.24](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.23...v0.3.24) (2023-12-23) ### Features * add last termination state when pod is in CrashloopBackoff ([#792](https://github.com/k8sgpt-ai/k8sgpt/issues/792)) ([ff4aaf7](https://github.com/k8sgpt-ai/k8sgpt/commit/ff4aaf7c328a58fcad8e4fb0f93ea543725eedd5)) * Add license scan report and status ([#796](https://github.com/k8sgpt-ai/k8sgpt/issues/796)) ([343aec8](https://github.com/k8sgpt-ai/k8sgpt/commit/343aec8f0455c9461eb8d495ca5bd446b4bad667)) * version upgrade to 1.21 ([#798](https://github.com/k8sgpt-ai/k8sgpt/issues/798)) ([c23f24d](https://github.com/k8sgpt-ai/k8sgpt/commit/c23f24de2e79347e4f5465e28af34e138cc13231)) ### Bug Fixes * added the ability to set the trivy variables by the user ([#797](https://github.com/k8sgpt-ai/k8sgpt/issues/797)) ([928b39a](https://github.com/k8sgpt-ai/k8sgpt/commit/928b39a7283ee274dd517e727624eceb3795594d)) * **deps:** update module cloud.google.com/go/storage to v1.36.0 ([#805](https://github.com/k8sgpt-ai/k8sgpt/issues/805)) ([390f309](https://github.com/k8sgpt-ai/k8sgpt/commit/390f30908800dfe21e2c1660139b0bd9d36b34d6)) * **deps:** update module github.com/aquasecurity/trivy-operator to v0.17.1 ([#780](https://github.com/k8sgpt-ai/k8sgpt/issues/780)) ([71f36bd](https://github.com/k8sgpt-ai/k8sgpt/commit/71f36bdb0b3729c4357299b7d03829dd5b6a69ec)) * **deps:** update module github.com/aws/aws-sdk-go to v1.49.6 ([#783](https://github.com/k8sgpt-ai/k8sgpt/issues/783)) ([1b386f6](https://github.com/k8sgpt-ai/k8sgpt/commit/1b386f64f2863d8a49f423ad571cba009807bc55)) * **deps:** update module github.com/aws/aws-sdk-go to v1.49.7 ([#804](https://github.com/k8sgpt-ai/k8sgpt/issues/804)) ([3c6c759](https://github.com/k8sgpt-ai/k8sgpt/commit/3c6c7597e014bfd68794b1764c3a8902e8a798ea)) * **deps:** update module github.com/aws/aws-sdk-go to v1.49.8 ([#807](https://github.com/k8sgpt-ai/k8sgpt/issues/807)) ([93b5ca1](https://github.com/k8sgpt-ai/k8sgpt/commit/93b5ca1985c3730592388ba6fc32ecca9b806888)) * **deps:** update module github.com/aws/aws-sdk-go to v1.49.9 ([#808](https://github.com/k8sgpt-ai/k8sgpt/issues/808)) ([130e4c2](https://github.com/k8sgpt-ai/k8sgpt/commit/130e4c2efd0e5b34cdc84c357c6c1f3987cf7c35)) * **deps:** update module github.com/azure/azure-sdk-for-go/sdk/storage/azblob to v1.2.1 ([#801](https://github.com/k8sgpt-ai/k8sgpt/issues/801)) ([aa05756](https://github.com/k8sgpt-ai/k8sgpt/commit/aa057565b5c971c493443f3ede4aed8f8a6399f7)) * **deps:** update module github.com/mittwald/go-helm-client to v0.12.5 ([#802](https://github.com/k8sgpt-ai/k8sgpt/issues/802)) ([4a7bad3](https://github.com/k8sgpt-ai/k8sgpt/commit/4a7bad313b66750bd830413b7fef005580ad843c)) * **deps:** update module github.com/sashabaranov/go-openai to v1.17.9 ([#772](https://github.com/k8sgpt-ai/k8sgpt/issues/772)) ([13d64a5](https://github.com/k8sgpt-ai/k8sgpt/commit/13d64a58750c7262c07042b557fbf2c4a511b777)) * **deps:** update module github.com/spf13/viper to v1.18.2 ([#787](https://github.com/k8sgpt-ai/k8sgpt/issues/787)) ([8dea617](https://github.com/k8sgpt-ai/k8sgpt/commit/8dea6170a2c00c03f08f25e4f0a232be617536f1)) * **deps:** update module google.golang.org/api to v0.154.0 ([#779](https://github.com/k8sgpt-ai/k8sgpt/issues/779)) ([78f7f2b](https://github.com/k8sgpt-ai/k8sgpt/commit/78f7f2ba85fd357cab13ccc15e9e767e8611773a)) * **deps:** update module google.golang.org/grpc to v1.60.1 ([#790](https://github.com/k8sgpt-ai/k8sgpt/issues/790)) ([5d54c3f](https://github.com/k8sgpt-ai/k8sgpt/commit/5d54c3f840a9ce002606b6601187e69fb62f8a28)) * **deps:** update module helm.sh/helm/v3 to v3.13.3 ([#803](https://github.com/k8sgpt-ai/k8sgpt/issues/803)) ([a8e1932](https://github.com/k8sgpt-ai/k8sgpt/commit/a8e193212222811f3a278df6056dd2165c4323bd)) * lowercase logs before running regex matching in LogAnalyzer ([#794](https://github.com/k8sgpt-ai/k8sgpt/issues/794)) ([03b63be](https://github.com/k8sgpt-ai/k8sgpt/commit/03b63befa247ac84b795a0ec8d5280196b8d570d)) ### Other * **deps:** update actions/setup-go action to v5 ([#788](https://github.com/k8sgpt-ai/k8sgpt/issues/788)) ([d00ed33](https://github.com/k8sgpt-ai/k8sgpt/commit/d00ed33678b1560a3996f1d735d84ca0ca05c0b0)) * **deps:** update actions/upload-artifact action to v4 ([#806](https://github.com/k8sgpt-ai/k8sgpt/issues/806)) ([d6fb648](https://github.com/k8sgpt-ai/k8sgpt/commit/d6fb648e23c1ed1e4680fc4b7b4e96501f50ad48)) * **deps:** update anchore/sbom-action action to v0.15.1 ([#784](https://github.com/k8sgpt-ai/k8sgpt/issues/784)) ([6473a2b](https://github.com/k8sgpt-ai/k8sgpt/commit/6473a2b532491b707b3af922fc2198e626ebf219)) * **deps:** update google-github-actions/release-please-action action to v4 ([#782](https://github.com/k8sgpt-ai/k8sgpt/issues/782)) ([2c28c55](https://github.com/k8sgpt-ai/k8sgpt/commit/2c28c555cf4e891b90ebd9e9eae1cd8724e9886f)) * **deps:** update google-github-actions/release-please-action action to v4.0.2 ([#800](https://github.com/k8sgpt-ai/k8sgpt/issues/800)) ([be4b0bb](https://github.com/k8sgpt-ai/k8sgpt/commit/be4b0bb3c24e04d35f40d16fd8e94ddbc8457ca6)) ### Refactoring * replace rest client with controller-runtime clientset for Trivy analyzers ([#776](https://github.com/k8sgpt-ai/k8sgpt/issues/776)) ([1d19628](https://github.com/k8sgpt-ai/k8sgpt/commit/1d196286b75f0ea6c068e8bdb01455fb36c52432)) ## [0.3.23](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.22...v0.3.23) (2023-11-24) ### Features * add Gateway analysers ([#764](https://github.com/k8sgpt-ai/k8sgpt/issues/764)) ([ec08cac](https://github.com/k8sgpt-ai/k8sgpt/commit/ec08cac21496b34b123b75b06d9283eb6539e890)) ### Bug Fixes * **deps:** update module github.com/aws/aws-sdk-go to v1.48.3 ([#768](https://github.com/k8sgpt-ai/k8sgpt/issues/768)) ([b1c791a](https://github.com/k8sgpt-ai/k8sgpt/commit/b1c791a396b7287ef916e8f8d382a0e14ba39949)) * **deps:** update module github.com/mittwald/go-helm-client to v0.12.4 ([#767](https://github.com/k8sgpt-ai/k8sgpt/issues/767)) ([dca5b47](https://github.com/k8sgpt-ai/k8sgpt/commit/dca5b4710d1bb35dfc3346219d3bddb7c726300e)) ## [0.3.22](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.21...v0.3.22) (2023-11-21) ### Features * rework cache package - add gcs cache - add cache purge command ([#750](https://github.com/k8sgpt-ai/k8sgpt/issues/750)) ([12146bf](https://github.com/k8sgpt-ai/k8sgpt/commit/12146bf356a3b26176c47e3a013a713fd14f346d)) ### Bug Fixes * cover more error reason messages ([#759](https://github.com/k8sgpt-ai/k8sgpt/issues/759)) ([5b27c3e](https://github.com/k8sgpt-ai/k8sgpt/commit/5b27c3e352701819f1d0449df9acf706040f1f13)) * **deps:** update kubernetes packages to v0.28.4 ([#756](https://github.com/k8sgpt-ai/k8sgpt/issues/756)) ([24132c2](https://github.com/k8sgpt-ai/k8sgpt/commit/24132c2d87024157009589cf2bd410bac2a26241)) * **deps:** update module cloud.google.com/go/storage to v1.35.1 ([#762](https://github.com/k8sgpt-ai/k8sgpt/issues/762)) ([58d182e](https://github.com/k8sgpt-ai/k8sgpt/commit/58d182e94f75f9b035a9e45159fa87ce8a57de38)) * **deps:** update module github.com/aquasecurity/trivy-operator to v0.16.4 ([#676](https://github.com/k8sgpt-ai/k8sgpt/issues/676)) ([4531278](https://github.com/k8sgpt-ai/k8sgpt/commit/45312788c3c15e141027c3fc8e428cfaa71d3ace)) * **deps:** update module github.com/aws/aws-sdk-go to v1.47.10 ([#751](https://github.com/k8sgpt-ai/k8sgpt/issues/751)) ([2aa31bc](https://github.com/k8sgpt-ai/k8sgpt/commit/2aa31bc66d239906b1047f53bcaa58b0c30a2856)) * **deps:** update module github.com/aws/aws-sdk-go to v1.47.11 ([#752](https://github.com/k8sgpt-ai/k8sgpt/issues/752)) ([531fa79](https://github.com/k8sgpt-ai/k8sgpt/commit/531fa79ed640846b177c516559dc82f088fa940f)) * **deps:** update module github.com/aws/aws-sdk-go to v1.48.0 ([#754](https://github.com/k8sgpt-ai/k8sgpt/issues/754)) ([e2bb567](https://github.com/k8sgpt-ai/k8sgpt/commit/e2bb567d2f8d59a904583309c2774d4174eb367f)) * **deps:** update module github.com/aws/aws-sdk-go to v1.48.1 ([#766](https://github.com/k8sgpt-ai/k8sgpt/issues/766)) ([16469c0](https://github.com/k8sgpt-ai/k8sgpt/commit/16469c01c962fd5bfa4ad11dd88a41f3e00e4a0d)) * **deps:** update module github.com/sashabaranov/go-openai to v1.17.6 ([#749](https://github.com/k8sgpt-ai/k8sgpt/issues/749)) ([84df364](https://github.com/k8sgpt-ai/k8sgpt/commit/84df3640bc114bb2c768f158d3575732103ff799)) * **deps:** update module github.com/sashabaranov/go-openai to v1.17.7 ([#753](https://github.com/k8sgpt-ai/k8sgpt/issues/753)) ([9971699](https://github.com/k8sgpt-ai/k8sgpt/commit/9971699fcf42b3309449d81875d45180f723de8d)) * **deps:** update module github.com/sashabaranov/go-openai to v1.17.8 ([#761](https://github.com/k8sgpt-ai/k8sgpt/issues/761)) ([beaa532](https://github.com/k8sgpt-ai/k8sgpt/commit/beaa53251c8201028db83d60f208e2b0658c93d8)) * **deps:** update module google.golang.org/api to v0.151.0 ([#763](https://github.com/k8sgpt-ai/k8sgpt/issues/763)) ([3e3f6a9](https://github.com/k8sgpt-ai/k8sgpt/commit/3e3f6a903a81d9622660f5adf9cae7d22a5c99f4)) * show trivy as active when activated with --no-install flag ([#675](https://github.com/k8sgpt-ai/k8sgpt/issues/675)) ([7368271](https://github.com/k8sgpt-ai/k8sgpt/commit/73682717eda4fa2e0cbc6311d5c97e01e0f2673c)) ### Other * **deps:** update anchore/sbom-action action to v0.15.0 ([#765](https://github.com/k8sgpt-ai/k8sgpt/issues/765)) ([cf1e243](https://github.com/k8sgpt-ai/k8sgpt/commit/cf1e243708ab406f070da3f96be1fc60b7ce2ea4)) * **deps:** update docker/build-push-action digest to 4a13e50 ([#760](https://github.com/k8sgpt-ai/k8sgpt/issues/760)) ([b5853de](https://github.com/k8sgpt-ai/k8sgpt/commit/b5853de8a6fcd17b1c1a4c53dbe3ffc82b83f72f)) ## [0.3.21](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.20...v0.3.21) (2023-11-12) ### Features * auth remove: add -b flag ([#711](https://github.com/k8sgpt-ai/k8sgpt/issues/711)) ([9dadd18](https://github.com/k8sgpt-ai/k8sgpt/commit/9dadd186c8d03a4284faff3f0842d6e2d00ebbb8)) * log analyzer ([#744](https://github.com/k8sgpt-ai/k8sgpt/issues/744)) ([d365886](https://github.com/k8sgpt-ai/k8sgpt/commit/d365886753f785bd58118c03510696318ea47941)) ### Bug Fixes * **deps:** update module github.com/aws/aws-sdk-go to v1.47.6 ([#728](https://github.com/k8sgpt-ai/k8sgpt/issues/728)) ([bb21ce8](https://github.com/k8sgpt-ai/k8sgpt/commit/bb21ce80c782e011dfa1f808ccdd82ae748bfed8)) * **deps:** update module github.com/aws/aws-sdk-go to v1.47.8 ([#741](https://github.com/k8sgpt-ai/k8sgpt/issues/741)) ([d359caa](https://github.com/k8sgpt-ai/k8sgpt/commit/d359caaab6bdb42a54d305be2f4cd8452f512bb8)) * **deps:** update module github.com/aws/aws-sdk-go to v1.47.9 ([#743](https://github.com/k8sgpt-ai/k8sgpt/issues/743)) ([45ebad7](https://github.com/k8sgpt-ai/k8sgpt/commit/45ebad7b4d80d93920d5fbad9f42c8fcd45218bd)) * **deps:** update module github.com/fatih/color to v1.16.0 ([#734](https://github.com/k8sgpt-ai/k8sgpt/issues/734)) ([8ab26d9](https://github.com/k8sgpt-ai/k8sgpt/commit/8ab26d96cec73369ecf014d50fccc26afe15fa44)) * **deps:** update module github.com/sashabaranov/go-openai to v1.17.3 ([#737](https://github.com/k8sgpt-ai/k8sgpt/issues/737)) ([48486e9](https://github.com/k8sgpt-ai/k8sgpt/commit/48486e96274a5e52a03cef00bd531148e27b38c5)) * **deps:** update module github.com/sashabaranov/go-openai to v1.17.5 ([#742](https://github.com/k8sgpt-ai/k8sgpt/issues/742)) ([3bff9cb](https://github.com/k8sgpt-ai/k8sgpt/commit/3bff9cbe7bb3afb7212735eb91902fd83d3cbb8c)) * **deps:** update module github.com/schollz/progressbar/v3 to v3.14.1 ([#738](https://github.com/k8sgpt-ai/k8sgpt/issues/738)) ([05f444d](https://github.com/k8sgpt-ai/k8sgpt/commit/05f444dec1f234c191e25f71f3eab4838eb2477a)) * **deps:** update module github.com/spf13/cobra to v1.8.0 ([#732](https://github.com/k8sgpt-ai/k8sgpt/issues/732)) ([19e502a](https://github.com/k8sgpt-ai/k8sgpt/commit/19e502a841e0463b682b0c6b8291f10aee616d7e)) * **deps:** update module helm.sh/helm/v3 to v3.13.2 ([#740](https://github.com/k8sgpt-ai/k8sgpt/issues/740)) ([6a665f0](https://github.com/k8sgpt-ai/k8sgpt/commit/6a665f05d782ba9c3051df7a15ff304c89cb34f4)) ### Other * **deps:** pin google-github-actions/release-please-action action to db8f2c6 ([#747](https://github.com/k8sgpt-ai/k8sgpt/issues/747)) ([4408110](https://github.com/k8sgpt-ai/k8sgpt/commit/4408110b1a4835bb237b3d5674d6fa8a13f0181b)) * **deps:** update google-github-actions/release-please-action digest to 4c5670f ([#721](https://github.com/k8sgpt-ai/k8sgpt/issues/721)) ([9c518ba](https://github.com/k8sgpt-ai/k8sgpt/commit/9c518badf53e4ccd9c2f9251cead4692602c0762)) * **deps:** update google-github-actions/release-please-action digest to db8f2c6 ([#736](https://github.com/k8sgpt-ai/k8sgpt/issues/736)) ([fdb2934](https://github.com/k8sgpt-ai/k8sgpt/commit/fdb2934e8fd02bcb4e47b34c1eca5b099f462faa)) * enable automerge for renovate ([#745](https://github.com/k8sgpt-ai/k8sgpt/issues/745)) ([66ebb88](https://github.com/k8sgpt-ai/k8sgpt/commit/66ebb88efe1ad5ecae75a5299f58a1e68179b515)) * pin release-please version ([#746](https://github.com/k8sgpt-ai/k8sgpt/issues/746)) ([c4925b2](https://github.com/k8sgpt-ai/k8sgpt/commit/c4925b2170546d0d86b77d2a13c13d4907e2e3d6)) ### Dependency Updates * bump docker fixes CVE GHSA-jq35-85cj-fj4p ([#733](https://github.com/k8sgpt-ai/k8sgpt/issues/733)) ([120027e](https://github.com/k8sgpt-ai/k8sgpt/commit/120027e3cbec2535f0b6cc8d8db1dc27dd9f3ec6)) ## [0.3.20](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.19...v0.3.20) (2023-11-05) ### Features * amazonsagemaker AI provider ([#731](https://github.com/k8sgpt-ai/k8sgpt/issues/731)) ([ccef7f6](https://github.com/k8sgpt-ai/k8sgpt/commit/ccef7f617004723b37d1e8ffb011398005e0b392)) ### Bug Fixes * **deps:** update module github.com/aws/aws-sdk-go to v1.47.1 ([#724](https://github.com/k8sgpt-ai/k8sgpt/issues/724)) ([0136b8f](https://github.com/k8sgpt-ai/k8sgpt/commit/0136b8f543a7052e967e29691afe1aab8e5fae1b)) * **deps:** update module github.com/azure/azure-sdk-for-go/sdk/storage/azblob to v1.2.0 ([#723](https://github.com/k8sgpt-ai/k8sgpt/issues/723)) ([16b229d](https://github.com/k8sgpt-ai/k8sgpt/commit/16b229d5478085655041ff0230d2542c4c0c7ce9)) * **deps:** update module google.golang.org/grpc to v1.59.0 ([#713](https://github.com/k8sgpt-ai/k8sgpt/issues/713)) ([901c5ec](https://github.com/k8sgpt-ai/k8sgpt/commit/901c5ec18858f2f7fd385ff20aef77d203748c93)) * **deps:** update module helm.sh/helm/v3 to v3.13.1 ([#706](https://github.com/k8sgpt-ai/k8sgpt/issues/706)) ([40133ad](https://github.com/k8sgpt-ai/k8sgpt/commit/40133adaedff3862199e00f62877a88fcffa67c5)) * ensure ingress HTTP rule exists to prevent panic ([#726](https://github.com/k8sgpt-ai/k8sgpt/issues/726)) ([37721b5](https://github.com/k8sgpt-ai/k8sgpt/commit/37721b5dd77d66edfb7e8377b2b96470b8a21d1b)) ### Other * **deps:** update amannn/action-semantic-pull-request action to v5.4.0 ([#729](https://github.com/k8sgpt-ai/k8sgpt/issues/729)) ([188a8a2](https://github.com/k8sgpt-ai/k8sgpt/commit/188a8a2cd5e25b35446e2eab46279a0ba3976af3)) ## [0.3.19](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.18...v0.3.19) (2023-10-28) ### Features * add amazonbedrock ([#718](https://github.com/k8sgpt-ai/k8sgpt/issues/718)) ([f1a7801](https://github.com/k8sgpt-ai/k8sgpt/commit/f1a7801e9e6a7e4a5310622951dfba3ba3acd047)) * add Azure remote cache ([#690](https://github.com/k8sgpt-ai/k8sgpt/issues/690)) ([23ac52d](https://github.com/k8sgpt-ai/k8sgpt/commit/23ac52d5ffc0b2ebb7516b070fa740108cb4299a)) ### Bug Fixes * **deps:** update kubernetes packages to v0.28.3 ([#715](https://github.com/k8sgpt-ai/k8sgpt/issues/715)) ([7e73f8a](https://github.com/k8sgpt-ai/k8sgpt/commit/7e73f8afbce7ba0e9de432671b88c01fcfe28c3a)) * **deps:** update module github.com/aws/aws-sdk-go to v1.45.25 ([#707](https://github.com/k8sgpt-ai/k8sgpt/issues/707)) ([3ebc867](https://github.com/k8sgpt-ai/k8sgpt/commit/3ebc86772dc8f8cb2d2246724f5fd05d1e931512)) * **deps:** update module github.com/aws/aws-sdk-go to v1.45.26 ([#709](https://github.com/k8sgpt-ai/k8sgpt/issues/709)) ([c977528](https://github.com/k8sgpt-ai/k8sgpt/commit/c977528ec7839902570785e0803f6c6b83a0a69d)) * **deps:** update module github.com/aws/aws-sdk-go to v1.46.5 ([#712](https://github.com/k8sgpt-ai/k8sgpt/issues/712)) ([63a2260](https://github.com/k8sgpt-ai/k8sgpt/commit/63a226065c8068f9bdc0aa791a325fa10bba3fcc)) * **deps:** update module github.com/azure/azure-sdk-for-go/sdk/azidentity to v1.4.0 ([#722](https://github.com/k8sgpt-ai/k8sgpt/issues/722)) ([0e7219a](https://github.com/k8sgpt-ai/k8sgpt/commit/0e7219a36aaa718b7d86adf0a218a521bfac119b)) * **deps:** update module github.com/sashabaranov/go-openai to v1.16.0 ([#703](https://github.com/k8sgpt-ai/k8sgpt/issues/703)) ([b5facd6](https://github.com/k8sgpt-ai/k8sgpt/commit/b5facd64a340a96d38faf045bbb889b928ef08a1)) * **deps:** update module github.com/spf13/viper to v1.17.0 ([#700](https://github.com/k8sgpt-ai/k8sgpt/issues/700)) ([184d148](https://github.com/k8sgpt-ai/k8sgpt/commit/184d1481081f4297bec21fbd60d7eff1964944ae)) * **deps:** update module google.golang.org/grpc to v1.58.3 ([#704](https://github.com/k8sgpt-ai/k8sgpt/issues/704)) ([1d7360c](https://github.com/k8sgpt-ai/k8sgpt/commit/1d7360c0ae4dab376872acc71dc68d59eb4d9752)) ### Other * **deps:** update actions/checkout digest to b4ffde6 ([#719](https://github.com/k8sgpt-ai/k8sgpt/issues/719)) ([a77bd41](https://github.com/k8sgpt-ai/k8sgpt/commit/a77bd410489e624d29ccc8fd45a004f6844b3620)) * **deps:** update module oras.land/oras-go to v1.2.4 ([#665](https://github.com/k8sgpt-ai/k8sgpt/issues/665)) ([4af0ad0](https://github.com/k8sgpt-ai/k8sgpt/commit/4af0ad0303d9b0ffb43f1e87fb5abe279d9a8724)) ## [0.3.18](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.17...v0.3.18) (2023-10-12) ### Features * adding temperature to server mode ([#705](https://github.com/k8sgpt-ai/k8sgpt/issues/705)) ([539ca3b](https://github.com/k8sgpt-ai/k8sgpt/commit/539ca3b78f96694c11f788255d3b83d2fb335df4)) ### Bug Fixes * **deps:** update module buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go to v1.3.0-20231002095256-194bc640518b.1 ([#692](https://github.com/k8sgpt-ai/k8sgpt/issues/692)) ([4d4e33b](https://github.com/k8sgpt-ai/k8sgpt/commit/4d4e33bea9cc4f5f9bf5379db5b890d9ba86e0a9)) * **deps:** update module buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go to v1.31.0-20231002095256-194bc640518b.1 ([#693](https://github.com/k8sgpt-ai/k8sgpt/issues/693)) ([20e6bd8](https://github.com/k8sgpt-ai/k8sgpt/commit/20e6bd816f636d4e4c8274d417870ec28fdd8a56)) * **deps:** update module github.com/aws/aws-sdk-go to v1.45.20 ([#685](https://github.com/k8sgpt-ai/k8sgpt/issues/685)) ([2494946](https://github.com/k8sgpt-ai/k8sgpt/commit/2494946dc867a532460bd6aac74dfb7da5184c1c)) * **deps:** update module github.com/aws/aws-sdk-go to v1.45.21 ([#696](https://github.com/k8sgpt-ai/k8sgpt/issues/696)) ([95c8cc0](https://github.com/k8sgpt-ai/k8sgpt/commit/95c8cc0afb0bb7b99784dcc5ba155f94b5a7dbdf)) * **deps:** update module github.com/aws/aws-sdk-go to v1.45.22 ([#697](https://github.com/k8sgpt-ai/k8sgpt/issues/697)) ([923a8c1](https://github.com/k8sgpt-ai/k8sgpt/commit/923a8c13c06b152d04e8b00ab002e2036bf12740)) * **deps:** update module github.com/aws/aws-sdk-go to v1.45.23 ([#699](https://github.com/k8sgpt-ai/k8sgpt/issues/699)) ([3f36a44](https://github.com/k8sgpt-ai/k8sgpt/commit/3f36a4441532e3d0ac1bd9d00fc738d4902b23a8)) * **deps:** update module github.com/aws/aws-sdk-go to v1.45.24 ([#701](https://github.com/k8sgpt-ai/k8sgpt/issues/701)) ([6d3038b](https://github.com/k8sgpt-ai/k8sgpt/commit/6d3038b0e8336235dc6a2fdb69d2381790331596)) * **deps:** update module github.com/prometheus/client_golang to v1.17.0 ([#687](https://github.com/k8sgpt-ai/k8sgpt/issues/687)) ([9597002](https://github.com/k8sgpt-ai/k8sgpt/commit/95970027237e0079ed1f66dc9655fa01b181f4d7)) * **deps:** update module github.com/sashabaranov/go-openai to v1.15.4 ([#689](https://github.com/k8sgpt-ai/k8sgpt/issues/689)) ([f11d314](https://github.com/k8sgpt-ai/k8sgpt/commit/f11d3149b228b643155ed66c189cb0f8a4dd5a0f)) * **deps:** update module helm.sh/helm/v3 to v3.13.0 ([#688](https://github.com/k8sgpt-ai/k8sgpt/issues/688)) ([87c8bce](https://github.com/k8sgpt-ai/k8sgpt/commit/87c8bcea4becd165aeb0ac98d79df7dab9c37ee3)) * security warning around printing provider details in https://github.com/k8sgpt-ai/k8sgpt/security/code-scanning/1 ([#695](https://github.com/k8sgpt-ai/k8sgpt/issues/695)) ([85ce557](https://github.com/k8sgpt-ai/k8sgpt/commit/85ce55768199f90b1d2a5118ec2621ea5c7a7a67)) ### Other * **deps:** update amannn/action-semantic-pull-request action to v5.3.0 ([#683](https://github.com/k8sgpt-ai/k8sgpt/issues/683)) ([c5a8c46](https://github.com/k8sgpt-ai/k8sgpt/commit/c5a8c462989c097bf37ac48ea4f1a9010285042c)) * fixing default model issue ([#702](https://github.com/k8sgpt-ai/k8sgpt/issues/702)) ([2a34ff2](https://github.com/k8sgpt-ai/k8sgpt/commit/2a34ff24d1f391270ae42531807cb1422880ad27)) ## [0.3.17](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.16...v0.3.17) (2023-09-28) ### Features * added create namespace on deploy ([#673](https://github.com/k8sgpt-ai/k8sgpt/issues/673)) ([820e475](https://github.com/k8sgpt-ai/k8sgpt/commit/820e4755a54ecab3b5d800017bf6948dc9212825)) * integration refactor ([#684](https://github.com/k8sgpt-ai/k8sgpt/issues/684)) ([69fe2db](https://github.com/k8sgpt-ai/k8sgpt/commit/69fe2db8acb795add27f04c1c8ee8d05819300ac)) * update readme with new analyzers ([#671](https://github.com/k8sgpt-ai/k8sgpt/issues/671)) ([cad605a](https://github.com/k8sgpt-ai/k8sgpt/commit/cad605af462ce8b02ffc279ea847e41b7a64196f)) ### Bug Fixes * **deps:** update kubernetes packages to v0.28.2 ([#607](https://github.com/k8sgpt-ai/k8sgpt/issues/607)) ([ddeff9f](https://github.com/k8sgpt-ai/k8sgpt/commit/ddeff9fae4e80d1452893c59b89742633eb6b51b)) * **deps:** update module github.com/aws/aws-sdk-go to v1.45.14 ([#672](https://github.com/k8sgpt-ai/k8sgpt/issues/672)) ([1da4b7c](https://github.com/k8sgpt-ai/k8sgpt/commit/1da4b7c8f0eee877d5b76a7dd9abda7631d922f3)) * **deps:** update module github.com/aws/aws-sdk-go to v1.45.16 ([#682](https://github.com/k8sgpt-ai/k8sgpt/issues/682)) ([e1a42ff](https://github.com/k8sgpt-ai/k8sgpt/commit/e1a42ff3bcb3ddea71df2a5b5288eade024684dc)) * **deps:** update module github.com/google/gnostic to v0.7.0 ([#679](https://github.com/k8sgpt-ai/k8sgpt/issues/679)) ([901ffb8](https://github.com/k8sgpt-ai/k8sgpt/commit/901ffb8df451ce41e6dc96da61deab987e51b6df)) * **deps:** update module google.golang.org/grpc to v1.58.2 ([#680](https://github.com/k8sgpt-ai/k8sgpt/issues/680)) ([402e97d](https://github.com/k8sgpt-ai/k8sgpt/commit/402e97d05ea33879d997d98019b72da0f1074fc7)) ### Other * **deps:** update actions/checkout digest to 8ade135 ([#681](https://github.com/k8sgpt-ai/k8sgpt/issues/681)) ([aa9e6a3](https://github.com/k8sgpt-ai/k8sgpt/commit/aa9e6a3549877260423462c35ebbdfd95381be2c)) ## [0.3.16](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.15...v0.3.16) (2023-09-19) ### Features * lists activate integrations ([#669](https://github.com/k8sgpt-ai/k8sgpt/issues/669)) ([844ff1f](https://github.com/k8sgpt-ai/k8sgpt/commit/844ff1fc78e7c35837c08b72bd2c19e92698d53d)) * openAI explicit value for maxToken and temperature ([#659](https://github.com/k8sgpt-ai/k8sgpt/issues/659)) ([f55946d](https://github.com/k8sgpt-ai/k8sgpt/commit/f55946d60ebc7725aba6702570ca1cb5ba978d78)) * serve/integration capability ([#645](https://github.com/k8sgpt-ai/k8sgpt/issues/645)) ([ab064b9](https://github.com/k8sgpt-ai/k8sgpt/commit/ab064b940cdb39a1588816221b20191e68263c61)) ### Bug Fixes * **deps:** update module buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go to v1.3.0-20230830164712-dc062a152c20.1 ([#617](https://github.com/k8sgpt-ai/k8sgpt/issues/617)) ([d6b7b81](https://github.com/k8sgpt-ai/k8sgpt/commit/d6b7b818aef1b7775d1e76231077b74481546c56)) * **deps:** update module github.com/aws/aws-sdk-go to v1.45.10 ([#657](https://github.com/k8sgpt-ai/k8sgpt/issues/657)) ([0325724](https://github.com/k8sgpt-ai/k8sgpt/commit/03257246589ebbb22961e13394e49b52cb056e38)) * **deps:** update module github.com/aws/aws-sdk-go to v1.45.11 ([#662](https://github.com/k8sgpt-ai/k8sgpt/issues/662)) ([1b80b0c](https://github.com/k8sgpt-ai/k8sgpt/commit/1b80b0ce95f39c1cf27ad8bbb05a7fed10322114)) * **deps:** update module github.com/aws/aws-sdk-go to v1.45.12 ([#666](https://github.com/k8sgpt-ai/k8sgpt/issues/666)) ([b4656f5](https://github.com/k8sgpt-ai/k8sgpt/commit/b4656f533bdf39d12b223158bf41087076fa6c9a)) * **deps:** update module github.com/sashabaranov/go-openai to v1.15.3 ([#636](https://github.com/k8sgpt-ai/k8sgpt/issues/636)) ([54caff8](https://github.com/k8sgpt-ai/k8sgpt/commit/54caff837dc25ae594c6cd0e1bd0b31b1612cf73)) * **deps:** update module go.uber.org/zap to v1.26.0 ([#658](https://github.com/k8sgpt-ai/k8sgpt/issues/658)) ([f76b572](https://github.com/k8sgpt-ai/k8sgpt/commit/f76b57265432a704c3fc5aa67b0d569179b4ef03)) * **deps:** update module google.golang.org/grpc to v1.58.0 ([#635](https://github.com/k8sgpt-ai/k8sgpt/issues/635)) ([d58e002](https://github.com/k8sgpt-ai/k8sgpt/commit/d58e002d7dc55cc759402fcadb03af921cd30dc3)) * **deps:** update module google.golang.org/grpc to v1.58.1 ([#656](https://github.com/k8sgpt-ai/k8sgpt/issues/656)) ([abfb584](https://github.com/k8sgpt-ai/k8sgpt/commit/abfb58432fbd1168db13880e5b9dbcbdde70f147)) * emergency fix for bad package revision in go mod ([#663](https://github.com/k8sgpt-ai/k8sgpt/issues/663)) ([2472da1](https://github.com/k8sgpt-ai/k8sgpt/commit/2472da167300a831dc5b45f7fc0169a0b5b1ccb7)) * pdb panic error guard ([#664](https://github.com/k8sgpt-ai/k8sgpt/issues/664)) ([3277b2a](https://github.com/k8sgpt-ai/k8sgpt/commit/3277b2ad4b27ade9bd7da07f5fc8d8f074355177)) * respect namespace scope in trivy analyzer ([#661](https://github.com/k8sgpt-ai/k8sgpt/issues/661)) ([6481590](https://github.com/k8sgpt-ai/k8sgpt/commit/6481590b29b80391ea1c9298cae5d8f0a4ae7354)) * use default values when adding auth ([#568](https://github.com/k8sgpt-ai/k8sgpt/issues/568)) ([7461a74](https://github.com/k8sgpt-ai/k8sgpt/commit/7461a748f8e994e58ac4f56fd9919b1744bd7366)), closes [#567](https://github.com/k8sgpt-ai/k8sgpt/issues/567) ### Other * **deps:** update actions/upload-artifact digest to a8a3f3a ([#633](https://github.com/k8sgpt-ai/k8sgpt/issues/633)) ([4bfc7f9](https://github.com/k8sgpt-ai/k8sgpt/commit/4bfc7f996c851adadc5ab0754da6852979084e9d)) * **deps:** update reviewdog/action-golangci-lint digest to 24d4af2 ([#642](https://github.com/k8sgpt-ai/k8sgpt/issues/642)) ([f607360](https://github.com/k8sgpt-ai/k8sgpt/commit/f60736035b2601650f4b3ee352f16d1e57d6ec64)) ## [0.3.15](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.14...v0.3.15) (2023-09-14) ### Features * show each ConfigAuditReport check ([#646](https://github.com/k8sgpt-ai/k8sgpt/issues/646)) ([230eace](https://github.com/k8sgpt-ai/k8sgpt/commit/230eace18737a81e4c023826ffef1a9b1e17d4fd)) ### Bug Fixes * defer to service analyser when selectors are missing ([#652](https://github.com/k8sgpt-ai/k8sgpt/issues/652)) ([6c5a062](https://github.com/k8sgpt-ai/k8sgpt/commit/6c5a0628e4a8c493beae85049448e6e6588d63be)) * **deps:** update module github.com/aws/aws-sdk-go to v1.44.333 ([#611](https://github.com/k8sgpt-ai/k8sgpt/issues/611)) ([96d97cf](https://github.com/k8sgpt-ai/k8sgpt/commit/96d97cfa30c4d3c75facda3d3016c080dfa86eaa)) * **deps:** update module github.com/aws/aws-sdk-go to v1.45.0 ([#618](https://github.com/k8sgpt-ai/k8sgpt/issues/618)) ([632fc9a](https://github.com/k8sgpt-ai/k8sgpt/commit/632fc9a99fd0482dcff0768211c49bffb2e4032a)) * **deps:** update module github.com/aws/aws-sdk-go to v1.45.1 ([#624](https://github.com/k8sgpt-ai/k8sgpt/issues/624)) ([09984c2](https://github.com/k8sgpt-ai/k8sgpt/commit/09984c245de40fc7794f85a9535af4f8e5f5e776)) * **deps:** update module github.com/aws/aws-sdk-go to v1.45.2 ([#625](https://github.com/k8sgpt-ai/k8sgpt/issues/625)) ([b6498ef](https://github.com/k8sgpt-ai/k8sgpt/commit/b6498ef269919c61004dd860ebf08ed7f28810f7)) * **deps:** update module github.com/aws/aws-sdk-go to v1.45.3 ([#632](https://github.com/k8sgpt-ai/k8sgpt/issues/632)) ([5f73240](https://github.com/k8sgpt-ai/k8sgpt/commit/5f73240a0615e58a37e9eb00784628621bc1dfa1)) * **deps:** update module github.com/aws/aws-sdk-go to v1.45.6 ([#634](https://github.com/k8sgpt-ai/k8sgpt/issues/634)) ([3aabb48](https://github.com/k8sgpt-ai/k8sgpt/commit/3aabb4842d96ec14e61842847dc2feb3e3f31a0a)) * **deps:** update module github.com/aws/aws-sdk-go to v1.45.9 ([#640](https://github.com/k8sgpt-ai/k8sgpt/issues/640)) ([95787f2](https://github.com/k8sgpt-ai/k8sgpt/commit/95787f2854c4e4a971b2d687d97a5ceca30b9d5e)) * **deps:** update module github.com/sashabaranov/go-openai to v1.15.1 ([#622](https://github.com/k8sgpt-ai/k8sgpt/issues/622)) ([fc90dc8](https://github.com/k8sgpt-ai/k8sgpt/commit/fc90dc865b48fae99253b8bb6a8b1ae7047170b4)) * **deps:** update module golang.org/x/term to v0.12.0 ([#626](https://github.com/k8sgpt-ai/k8sgpt/issues/626)) ([44d17c5](https://github.com/k8sgpt-ai/k8sgpt/commit/44d17c51ff8ece92cd0c85f40d15caa97d990544)) * typos ([#629](https://github.com/k8sgpt-ai/k8sgpt/issues/629)) ([067c348](https://github.com/k8sgpt-ai/k8sgpt/commit/067c3483e6b379bd710c7f799de63bc1890b6c81)) * use-case while in cluster, connecting to an external ([#623](https://github.com/k8sgpt-ai/k8sgpt/issues/623)) ([1a7f45c](https://github.com/k8sgpt-ai/k8sgpt/commit/1a7f45cc55348d567148d01e61c7527e4d534f34)) ### Other * **deps:** bump github.com/cyphar/filepath-securejoin ([#644](https://github.com/k8sgpt-ai/k8sgpt/issues/644)) ([25890e6](https://github.com/k8sgpt-ai/k8sgpt/commit/25890e6e3807171e655fec0d2081cedad3ad6273)) * **deps:** update actions/checkout action to v4 ([#628](https://github.com/k8sgpt-ai/k8sgpt/issues/628)) ([e65d9a6](https://github.com/k8sgpt-ai/k8sgpt/commit/e65d9a650522120d602b2a62703aa2b39abfdea1)) * **deps:** update actions/checkout digest to f43a0e5 ([#612](https://github.com/k8sgpt-ai/k8sgpt/issues/612)) ([6f9f7b2](https://github.com/k8sgpt-ai/k8sgpt/commit/6f9f7b2b602605f3be7fd02bd521574e9c26fa78)) * **deps:** update docker/build-push-action action to v5 ([#643](https://github.com/k8sgpt-ai/k8sgpt/issues/643)) ([241f1bd](https://github.com/k8sgpt-ai/k8sgpt/commit/241f1bd6dfcb772711551aac42e48a2f59e64046)) * **deps:** update docker/login-action action to v3 ([#648](https://github.com/k8sgpt-ai/k8sgpt/issues/648)) ([b491c92](https://github.com/k8sgpt-ai/k8sgpt/commit/b491c9200e781284737dd74a9789dfc0c1e7b14a)) * **deps:** update docker/setup-buildx-action action to v3 ([#649](https://github.com/k8sgpt-ai/k8sgpt/issues/649)) ([598ef22](https://github.com/k8sgpt-ai/k8sgpt/commit/598ef22e570c1db678d583638c83e242f3b313d7)) * **deps:** update docker/setup-buildx-action digest to 885d146 ([#615](https://github.com/k8sgpt-ai/k8sgpt/issues/615)) ([2c81dad](https://github.com/k8sgpt-ai/k8sgpt/commit/2c81dadb4d4abcdc3608be768c1f3aae87e53a68)) * **deps:** update goreleaser/goreleaser-action action to v5 ([#641](https://github.com/k8sgpt-ai/k8sgpt/issues/641)) ([00d7a27](https://github.com/k8sgpt-ai/k8sgpt/commit/00d7a27ec1ea1bd49ab1879b8ffa0b9e7c0b6adf)) * **deps:** update goreleaser/goreleaser-action digest to 5fdedb9 ([#631](https://github.com/k8sgpt-ai/k8sgpt/issues/631)) ([5de3b64](https://github.com/k8sgpt-ai/k8sgpt/commit/5de3b640988783df5a04db368f79b9b9eefdb8bf)) * fixes a bug where filters do not deactive ([#621](https://github.com/k8sgpt-ai/k8sgpt/issues/621)) ([133850f](https://github.com/k8sgpt-ai/k8sgpt/commit/133850f984cc0bb41ec1e4521a32ab30558778f1)) * slice loop replace ([#627](https://github.com/k8sgpt-ai/k8sgpt/issues/627)) ([c24825b](https://github.com/k8sgpt-ai/k8sgpt/commit/c24825b81025c5cd79224a79b52d6c5efdc00511)) * updated protobuf libs ([#614](https://github.com/k8sgpt-ai/k8sgpt/issues/614)) ([5e17e66](https://github.com/k8sgpt-ai/k8sgpt/commit/5e17e666659c0eb057562def70d491daa995e5a2)) * updated schema for integrations support ([#616](https://github.com/k8sgpt-ai/k8sgpt/issues/616)) ([8f0a2fd](https://github.com/k8sgpt-ai/k8sgpt/commit/8f0a2fd41d6705da4d1a1d288f3b6ce19711f30d)) ## [0.3.14](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.13...v0.3.14) (2023-08-25) ### Features * configauditreport ([#609](https://github.com/k8sgpt-ai/k8sgpt/issues/609)) ([44d3613](https://github.com/k8sgpt-ai/k8sgpt/commit/44d3613c1f950837c6b112ddde0dc3e90f73dc1b)) ### Bug Fixes * **deps:** update kubernetes packages to v0.27.4 ([#565](https://github.com/k8sgpt-ai/k8sgpt/issues/565)) ([3cc7aa5](https://github.com/k8sgpt-ai/k8sgpt/commit/3cc7aa56d8efc6e78badf3be1cb3d5726074156e)) * **deps:** update module github.com/aquasecurity/trivy-operator to v0.15.1 ([#576](https://github.com/k8sgpt-ai/k8sgpt/issues/576)) ([c364074](https://github.com/k8sgpt-ai/k8sgpt/commit/c3640744c5cbf036321a14b90c1fdefa17c5321d)) * **deps:** update module github.com/aws/aws-sdk-go to v1.44.304 ([#558](https://github.com/k8sgpt-ai/k8sgpt/issues/558)) ([cf9069e](https://github.com/k8sgpt-ai/k8sgpt/commit/cf9069ef572fea9a947d7de5b0c0e44f34620a69)) * **deps:** update module github.com/aws/aws-sdk-go to v1.44.307 ([#574](https://github.com/k8sgpt-ai/k8sgpt/issues/574)) ([8ae91ec](https://github.com/k8sgpt-ai/k8sgpt/commit/8ae91ec744d1fead3b0aa570c904e9e3ad5ab5ef)) * **deps:** update module github.com/aws/aws-sdk-go to v1.44.308 ([#579](https://github.com/k8sgpt-ai/k8sgpt/issues/579)) ([7e8668a](https://github.com/k8sgpt-ai/k8sgpt/commit/7e8668a56bb25b7da3957cf4c05847d022825c10)) * **deps:** update module github.com/aws/aws-sdk-go to v1.44.309 ([#584](https://github.com/k8sgpt-ai/k8sgpt/issues/584)) ([227e1cd](https://github.com/k8sgpt-ai/k8sgpt/commit/227e1cd69f38654126750902a89408643bdb30fb)) * **deps:** update module github.com/aws/aws-sdk-go to v1.44.312 ([#586](https://github.com/k8sgpt-ai/k8sgpt/issues/586)) ([aafac93](https://github.com/k8sgpt-ai/k8sgpt/commit/aafac9345fbab16b1fe23ea76d6c1c362c44c080)) * **deps:** update module github.com/aws/aws-sdk-go to v1.44.313 ([#587](https://github.com/k8sgpt-ai/k8sgpt/issues/587)) ([f1479ba](https://github.com/k8sgpt-ai/k8sgpt/commit/f1479babbaaf6770d4a106d80f22b2ffb736cbad)) * **deps:** update module github.com/aws/aws-sdk-go to v1.44.315 ([#588](https://github.com/k8sgpt-ai/k8sgpt/issues/588)) ([fe29361](https://github.com/k8sgpt-ai/k8sgpt/commit/fe29361e335f3d186dc3d7651823e9bb03649652)) * **deps:** update module github.com/aws/aws-sdk-go to v1.44.317 ([#591](https://github.com/k8sgpt-ai/k8sgpt/issues/591)) ([9802e82](https://github.com/k8sgpt-ai/k8sgpt/commit/9802e82ff54bc55b670e25f75c69a29a985c21ae)) * **deps:** update module github.com/aws/aws-sdk-go to v1.44.327 ([#597](https://github.com/k8sgpt-ai/k8sgpt/issues/597)) ([aee83b7](https://github.com/k8sgpt-ai/k8sgpt/commit/aee83b74b20117f136876ec426318914aee8c4d1)) * **deps:** update module github.com/aws/aws-sdk-go to v1.44.329 ([#610](https://github.com/k8sgpt-ai/k8sgpt/issues/610)) ([0e5be89](https://github.com/k8sgpt-ai/k8sgpt/commit/0e5be89e5ccb70e9e9a44ad70f161c7b344d04f2)) * **deps:** update module github.com/mittwald/go-helm-client to v0.12.3 ([#582](https://github.com/k8sgpt-ai/k8sgpt/issues/582)) ([c2770f3](https://github.com/k8sgpt-ai/k8sgpt/commit/c2770f38a6f0d3248747927155505db505f5e960)) * **deps:** update module github.com/sashabaranov/go-openai to v1.14.1 ([#573](https://github.com/k8sgpt-ai/k8sgpt/issues/573)) ([b52424a](https://github.com/k8sgpt-ai/k8sgpt/commit/b52424a9b1a554739cb8e08e296045c181d4041c)) * **deps:** update module github.com/sashabaranov/go-openai to v1.14.2 ([#603](https://github.com/k8sgpt-ai/k8sgpt/issues/603)) ([81fcf8b](https://github.com/k8sgpt-ai/k8sgpt/commit/81fcf8b5d46387eca7128b877c0652fdf4ed999c)) * **deps:** update module go.uber.org/zap to v1.25.0 ([#589](https://github.com/k8sgpt-ai/k8sgpt/issues/589)) ([9672cea](https://github.com/k8sgpt-ai/k8sgpt/commit/9672cea228de976772f453e6a05ce05456741de8)) * **deps:** update module golang.org/x/term to v0.11.0 ([#593](https://github.com/k8sgpt-ai/k8sgpt/issues/593)) ([7f109cd](https://github.com/k8sgpt-ai/k8sgpt/commit/7f109cdcfac00a329a53121287e44c2567af6b4a)) * **deps:** update module google.golang.org/grpc to v1.57.0 ([#585](https://github.com/k8sgpt-ai/k8sgpt/issues/585)) ([59897f3](https://github.com/k8sgpt-ai/k8sgpt/commit/59897f330a037f1e5de0f958dd93b826e2ce481d)) * **deps:** update module helm.sh/helm/v3 to v3.12.3 ([#602](https://github.com/k8sgpt-ai/k8sgpt/issues/602)) ([7910c9a](https://github.com/k8sgpt-ai/k8sgpt/commit/7910c9aa2c40f3c1837cce179dd1fc91a9744946)) * optimize analyze service ([#461](https://github.com/k8sgpt-ai/k8sgpt/issues/461)) ([cc665ea](https://github.com/k8sgpt-ai/k8sgpt/commit/cc665ea4f3f279c30c7dd7996786e6bdce88acc8)) * use kubeconfig file when user specify it ([#605](https://github.com/k8sgpt-ai/k8sgpt/issues/605)) ([e3b21ec](https://github.com/k8sgpt-ai/k8sgpt/commit/e3b21ec5ecd5f823470c2c2f570ed89a2c071b5a)), closes [#604](https://github.com/k8sgpt-ai/k8sgpt/issues/604) ### Other * **deps:** exclude retracted cohere-go versions ([#583](https://github.com/k8sgpt-ai/k8sgpt/issues/583)) ([f8a53a5](https://github.com/k8sgpt-ai/k8sgpt/commit/f8a53a5c035fd3e3598666d9792c4e1231f9838d)) * **deps:** update actions/setup-go digest to 93397be ([#600](https://github.com/k8sgpt-ai/k8sgpt/issues/600)) ([1a0ae1a](https://github.com/k8sgpt-ai/k8sgpt/commit/1a0ae1a086d328b1eaa70c412122427a6e8df2f5)) * **deps:** update google-github-actions/release-please-action digest to ca6063f ([#572](https://github.com/k8sgpt-ai/k8sgpt/issues/572)) ([fba1a8e](https://github.com/k8sgpt-ai/k8sgpt/commit/fba1a8ed8c7cc2f7b0aace246f8797ea6c27e455)) * **deps:** update goreleaser/goreleaser-action digest to 3fa32b8 ([#601](https://github.com/k8sgpt-ai/k8sgpt/issues/601)) ([610720a](https://github.com/k8sgpt-ai/k8sgpt/commit/610720a95c9d5eb49c77e7a929cd766a04e534a4)) * **deps:** update reviewdog/action-golangci-lint digest to 951dc8b ([#594](https://github.com/k8sgpt-ai/k8sgpt/issues/594)) ([9acaec0](https://github.com/k8sgpt-ai/k8sgpt/commit/9acaec00c4d084c4ec3e40e4a6a8b0136dcc4aa1)) * **deps:** update reviewdog/action-golangci-lint digest to f17c2e2 ([#598](https://github.com/k8sgpt-ai/k8sgpt/issues/598)) ([2251321](https://github.com/k8sgpt-ai/k8sgpt/commit/22513216960f06d572ec53480e290b1f4e5ff1d8)) * upgraded cohere backend ([#580](https://github.com/k8sgpt-ai/k8sgpt/issues/580)) ([43b0d70](https://github.com/k8sgpt-ai/k8sgpt/commit/43b0d707e7eac326594f5f6c7ab4c885772846d2)) ## [0.3.13](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.12...v0.3.13) (2023-07-20) ### Other * continue on absent service ([#569](https://github.com/k8sgpt-ai/k8sgpt/issues/569)) ([153d38d](https://github.com/k8sgpt-ai/k8sgpt/commit/153d38deb060cb84d606f8391e5700025ce02a9b)) ## [0.3.12](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.11...v0.3.12) (2023-07-19) ### Features * add Cohere backend ([#563](https://github.com/k8sgpt-ai/k8sgpt/issues/563)) ([781ecb7](https://github.com/k8sgpt-ai/k8sgpt/commit/781ecb7aad689e6709678c9690c112115e3cf6c7)) ### Bug Fixes * **deps:** update module github.com/aws/aws-sdk-go to v1.44.300 ([#554](https://github.com/k8sgpt-ai/k8sgpt/issues/554)) ([dc46333](https://github.com/k8sgpt-ai/k8sgpt/commit/dc463334bccdf16106cff4e688a83bf0984d6e27)) * **deps:** update module github.com/mittwald/go-helm-client to v0.12.2 ([#562](https://github.com/k8sgpt-ai/k8sgpt/issues/562)) ([2e0db55](https://github.com/k8sgpt-ai/k8sgpt/commit/2e0db553f92b5ca691b5957b180be35131ab4e2f)) * **deps:** update module google.golang.org/grpc to v1.56.2 ([#546](https://github.com/k8sgpt-ai/k8sgpt/issues/546)) ([cc83fe1](https://github.com/k8sgpt-ai/k8sgpt/commit/cc83fe19bafc87647fa0293189f90c84d2dd8edb)) * **deps:** update module helm.sh/helm/v3 to v3.12.2 ([#555](https://github.com/k8sgpt-ai/k8sgpt/issues/555)) ([9eb96c4](https://github.com/k8sgpt-ai/k8sgpt/commit/9eb96c495cdb1247b664de625a036902b5e156ff)) ### Other * fixing edge cases with missing wh service ([#561](https://github.com/k8sgpt-ai/k8sgpt/issues/561)) ([c422215](https://github.com/k8sgpt-ai/k8sgpt/commit/c42221512bfdab7ac792963d459bf9f8dac3954c)) ### Docs * fix readme for anonymization ([#559](https://github.com/k8sgpt-ai/k8sgpt/issues/559)) ([70bec05](https://github.com/k8sgpt-ai/k8sgpt/commit/70bec050d854be6f559065278d6a583d8e0e333b)) ## [0.3.11](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.10...v0.3.11) (2023-07-14) ### Features * admission webhooks ([#553](https://github.com/k8sgpt-ai/k8sgpt/issues/553)) ([06e8532](https://github.com/k8sgpt-ai/k8sgpt/commit/06e8532f88616a988a4e41ed8cdac62cf0f243a5)) ### Other * **deps:** update docker/setup-buildx-action digest to 4c0219f ([#547](https://github.com/k8sgpt-ai/k8sgpt/issues/547)) ([1a3f299](https://github.com/k8sgpt-ai/k8sgpt/commit/1a3f2992108e857f8c8c07eff16599d00b50110e)) ## [0.3.10](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.9...v0.3.10) (2023-07-12) ### Features * add Validating/Mutating webhook analyzer ([#548](https://github.com/k8sgpt-ai/k8sgpt/issues/548)) ([750a10d](https://github.com/k8sgpt-ai/k8sgpt/commit/750a10d44c59bc90de5241d1128ee74fa38bf350)) ### Bug Fixes * **deps:** update module github.com/aws/aws-sdk-go to v1.44.298 ([#545](https://github.com/k8sgpt-ai/k8sgpt/issues/545)) ([d1096dc](https://github.com/k8sgpt-ai/k8sgpt/commit/d1096dc31a692013f40980649e5cc2d402869ceb)) * **deps:** update module github.com/aws/aws-sdk-go to v1.44.299 ([#549](https://github.com/k8sgpt-ai/k8sgpt/issues/549)) ([ecd7790](https://github.com/k8sgpt-ai/k8sgpt/commit/ecd7790efe2ca88259451761202c90cb842ff04b)) * **deps:** update module github.com/sashabaranov/go-openai to v1.13.0 ([#399](https://github.com/k8sgpt-ai/k8sgpt/issues/399)) ([21df094](https://github.com/k8sgpt-ai/k8sgpt/commit/21df094bda31a14235fb2244e8cef74d3c92d919)) * **deps:** update module github.com/sashabaranov/go-openai to v1.14.0 ([#550](https://github.com/k8sgpt-ai/k8sgpt/issues/550)) ([9dcab94](https://github.com/k8sgpt-ai/k8sgpt/commit/9dcab945460e5972f895fa5302e3425750d635c7)) * **deps:** update module golang.org/x/term to v0.10.0 ([#542](https://github.com/k8sgpt-ai/k8sgpt/issues/542)) ([1276b3e](https://github.com/k8sgpt-ai/k8sgpt/commit/1276b3e89715b1cfb553e60d4f25592acef80a6f)) ## [0.3.9](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.8...v0.3.9) (2023-07-04) ### Features * details flag to list command ([#537](https://github.com/k8sgpt-ai/k8sgpt/issues/537)) ([2309b0d](https://github.com/k8sgpt-ai/k8sgpt/commit/2309b0dfe20e27b6afe283a6be21ad7a0652ac99)) * upgrading azure client impl ([#526](https://github.com/k8sgpt-ai/k8sgpt/issues/526)) ([367fe8f](https://github.com/k8sgpt-ai/k8sgpt/commit/367fe8f74c6a9e26f0d9c3b25a86093530fb85b2)) * upgrading the proto files to fix user issues ([#515](https://github.com/k8sgpt-ai/k8sgpt/issues/515)) ([c88fc88](https://github.com/k8sgpt-ai/k8sgpt/commit/c88fc889e4f6089e48f37d90e349d5c61ea0b952)) ### Bug Fixes * 'intergration' typos ([#508](https://github.com/k8sgpt-ai/k8sgpt/issues/508)) ([64b93c9](https://github.com/k8sgpt-ai/k8sgpt/commit/64b93c9116b6a7f82419f1c4fff98fa68b8c0aca)) * add --no-install for activate command ([#536](https://github.com/k8sgpt-ai/k8sgpt/issues/536)) ([1f5462c](https://github.com/k8sgpt-ai/k8sgpt/commit/1f5462c80bd04f63c2b55889c987634251635812)) * **deps:** update module buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go to v1.3.0-20230620082254-6f80f9533908.1 ([#516](https://github.com/k8sgpt-ai/k8sgpt/issues/516)) ([06e50d5](https://github.com/k8sgpt-ai/k8sgpt/commit/06e50d57db3aa2e5a68b093e2ba25c0e33dc6343)) * **deps:** update module github.com/aws/aws-sdk-go to v1.44.284 ([#501](https://github.com/k8sgpt-ai/k8sgpt/issues/501)) ([d87127a](https://github.com/k8sgpt-ai/k8sgpt/commit/d87127a309734847a56bf95c2e947e2270f94a88)) * **deps:** update module github.com/aws/aws-sdk-go to v1.44.286 ([#514](https://github.com/k8sgpt-ai/k8sgpt/issues/514)) ([b9cf522](https://github.com/k8sgpt-ai/k8sgpt/commit/b9cf5226853619655e98f2156bfd0b8513511bb3)) * **deps:** update module github.com/aws/aws-sdk-go to v1.44.288 ([#519](https://github.com/k8sgpt-ai/k8sgpt/issues/519)) ([57695b4](https://github.com/k8sgpt-ai/k8sgpt/commit/57695b44b6429319860a76e4e02016dafe3ed0b0)) * **deps:** update module github.com/aws/aws-sdk-go to v1.44.289 ([#524](https://github.com/k8sgpt-ai/k8sgpt/issues/524)) ([fafb695](https://github.com/k8sgpt-ai/k8sgpt/commit/fafb69544f4edda670bad6973332a20a7f0f055e)) * **deps:** update module github.com/aws/aws-sdk-go to v1.44.292 ([#530](https://github.com/k8sgpt-ai/k8sgpt/issues/530)) ([566f752](https://github.com/k8sgpt-ai/k8sgpt/commit/566f7525eef9f65dd2ab6a47bd0012bfb91e2a12)) * **deps:** update module github.com/aws/aws-sdk-go to v1.44.294 ([#535](https://github.com/k8sgpt-ai/k8sgpt/issues/535)) ([3067fa9](https://github.com/k8sgpt-ai/k8sgpt/commit/3067fa98f4a8990c9a930e53ad93f89cf35e0d62)) * **deps:** update module github.com/aws/aws-sdk-go to v1.44.295 ([#540](https://github.com/k8sgpt-ai/k8sgpt/issues/540)) ([767e4cb](https://github.com/k8sgpt-ai/k8sgpt/commit/767e4cbc4127e2017a3a9c4b182ccc833debc6a5)) * **deps:** update module github.com/prometheus/client_golang to v1.16.0 ([#507](https://github.com/k8sgpt-ai/k8sgpt/issues/507)) ([14e5691](https://github.com/k8sgpt-ai/k8sgpt/commit/14e5691190bf772c05477cbcb811ed71bec450a3)) * **deps:** update module google.golang.org/grpc to v1.56.0 ([#510](https://github.com/k8sgpt-ai/k8sgpt/issues/510)) ([f3e0b9b](https://github.com/k8sgpt-ai/k8sgpt/commit/f3e0b9b56d13397c79f57e76bdd6b741bb565fb4)) * **deps:** update module google.golang.org/grpc to v1.56.1 ([#520](https://github.com/k8sgpt-ai/k8sgpt/issues/520)) ([be52308](https://github.com/k8sgpt-ai/k8sgpt/commit/be52308c99f6aed73e2c20d260823795d45876f5)) * **deps:** update module helm.sh/helm/v3 to v3.12.1 ([#503](https://github.com/k8sgpt-ai/k8sgpt/issues/503)) ([0f03ddc](https://github.com/k8sgpt-ai/k8sgpt/commit/0f03ddcf0f5ec79bc6dbb74c654e0d8fac634a0a)) * displaying correct yaml config location on app start ([#521](https://github.com/k8sgpt-ai/k8sgpt/issues/521)) ([b7d4602](https://github.com/k8sgpt-ai/k8sgpt/commit/b7d4602cb8aaaa0c22a0a5941d8c6edad7c58db4)) * remove provider from default on delete ([#529](https://github.com/k8sgpt-ai/k8sgpt/issues/529)) ([5a983c4](https://github.com/k8sgpt-ai/k8sgpt/commit/5a983c4a0a511389e25cffe12999b903b85cd96d)) * typo in add command ([#539](https://github.com/k8sgpt-ai/k8sgpt/issues/539)) ([da750df](https://github.com/k8sgpt-ai/k8sgpt/commit/da750df16bde878f7619aa58ef5e7ef7d3173b2b)) ### Other * **deps:** update anchore/sbom-action action to v0.14.3 ([#517](https://github.com/k8sgpt-ai/k8sgpt/issues/517)) ([0521060](https://github.com/k8sgpt-ai/k8sgpt/commit/05210604109a6e892bb465df11038b8c24d68076)) * **deps:** update docker/build-push-action digest to 2eb1c19 ([#499](https://github.com/k8sgpt-ai/k8sgpt/issues/499)) ([9df75cc](https://github.com/k8sgpt-ai/k8sgpt/commit/9df75cc959f7ed23cae8e3761498ea6c56885788)) * **deps:** update docker/login-action digest to 465a078 ([#488](https://github.com/k8sgpt-ai/k8sgpt/issues/488)) ([c15a561](https://github.com/k8sgpt-ai/k8sgpt/commit/c15a561b635dc678bb8de15c6623914942475537)) * **deps:** update docker/setup-buildx-action digest to 16c0bc4 ([#532](https://github.com/k8sgpt-ai/k8sgpt/issues/532)) ([5662d59](https://github.com/k8sgpt-ai/k8sgpt/commit/5662d5932ff3beb8c1a31fc2088c5e703e90ec79)) * **deps:** update docker/setup-buildx-action digest to ecf9528 ([#498](https://github.com/k8sgpt-ai/k8sgpt/issues/498)) ([f4d7876](https://github.com/k8sgpt-ai/k8sgpt/commit/f4d78768388774f62d87acd89e71689535e538f7)) * **deps:** update google-github-actions/release-please-action digest to 8016a66 ([#523](https://github.com/k8sgpt-ai/k8sgpt/issues/523)) ([d56861d](https://github.com/k8sgpt-ai/k8sgpt/commit/d56861d4bad475da09992813fc256a0d99399eab)) * **deps:** update reviewdog/action-golangci-lint digest to 22adb9d ([#525](https://github.com/k8sgpt-ai/k8sgpt/issues/525)) ([3146754](https://github.com/k8sgpt-ai/k8sgpt/commit/314675477917063dcfb847880fb3186f8bdf32f6)) * **deps:** update reviewdog/action-golangci-lint digest to 994abff ([#513](https://github.com/k8sgpt-ai/k8sgpt/issues/513)) ([1819c3b](https://github.com/k8sgpt-ai/k8sgpt/commit/1819c3bf1512291cd637c115e8b82c9e0e8885a0)) * sorting out the dependency hell ([#518](https://github.com/k8sgpt-ai/k8sgpt/issues/518)) ([cd7807a](https://github.com/k8sgpt-ai/k8sgpt/commit/cd7807a48481f298422d9b1c8066b431fad3ae5a)) ## [0.3.8](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.7...v0.3.8) (2023-06-15) ### Features * fix for s3 cache from operator ([f6db6ce](https://github.com/k8sgpt-ai/k8sgpt/commit/f6db6ce86163dcb4b5ec4bd99b8a3842dd0c60bb)) ### Bug Fixes * **deps:** update kubernetes packages to v0.27.3 ([#504](https://github.com/k8sgpt-ai/k8sgpt/issues/504)) ([b1c6ec3](https://github.com/k8sgpt-ai/k8sgpt/commit/b1c6ec3c0919649bc551ec0047a1d9c2420f4264)) ## [0.3.7](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.6...v0.3.7) (2023-06-13) ### Features * add update to auth cmd ([#450](https://github.com/k8sgpt-ai/k8sgpt/issues/450)) ([01aeeb3](https://github.com/k8sgpt-ai/k8sgpt/commit/01aeeb35e2dab957d2909cd5cffc5a4a03e19664)) * support arbitrary uid for openshift environments ([#454](https://github.com/k8sgpt-ai/k8sgpt/issues/454)) ([92539ee](https://github.com/k8sgpt-ai/k8sgpt/commit/92539ee05d2d15e951742aaaf07f2defff3f79c5)) ### Bug Fixes * **deps:** update module github.com/aws/aws-sdk-go to v1.44.274 ([#474](https://github.com/k8sgpt-ai/k8sgpt/issues/474)) ([1a81227](https://github.com/k8sgpt-ai/k8sgpt/commit/1a81227d6148be59b7b9ae4e9ae5e2d9a5b7a9ae)) * **deps:** update module github.com/aws/aws-sdk-go to v1.44.275 ([#478](https://github.com/k8sgpt-ai/k8sgpt/issues/478)) ([705b54f](https://github.com/k8sgpt-ai/k8sgpt/commit/705b54fcd308ef1fc0bc870b5a0a32baa30767df)) * **deps:** update module github.com/aws/aws-sdk-go to v1.44.276 ([#482](https://github.com/k8sgpt-ai/k8sgpt/issues/482)) ([3f0aea1](https://github.com/k8sgpt-ai/k8sgpt/commit/3f0aea131e1e62655a10f6a51bf6238316dd6598)) * **deps:** update module github.com/aws/aws-sdk-go to v1.44.277 ([#485](https://github.com/k8sgpt-ai/k8sgpt/issues/485)) ([e2d5c2d](https://github.com/k8sgpt-ai/k8sgpt/commit/e2d5c2dee00e3411fa10bcaa4ae134b5671f45ab)) * **deps:** update module github.com/aws/aws-sdk-go to v1.44.280 ([#490](https://github.com/k8sgpt-ai/k8sgpt/issues/490)) ([04b4f56](https://github.com/k8sgpt-ai/k8sgpt/commit/04b4f56a667febf77c21838a618a1cd4a7f1e371)) * **deps:** update module github.com/aws/aws-sdk-go to v1.44.281 ([#496](https://github.com/k8sgpt-ai/k8sgpt/issues/496)) ([a6342c9](https://github.com/k8sgpt-ai/k8sgpt/commit/a6342c92830451e3110a54bac80a1b693984fcc8)) * **deps:** update module golang.org/x/term to v0.9.0 ([#497](https://github.com/k8sgpt-ai/k8sgpt/issues/497)) ([98b852a](https://github.com/k8sgpt-ai/k8sgpt/commit/98b852aabe1ff62ac64e3c9e3e70173a8ff19749)) * use the `status` for pdb checking ([#477](https://github.com/k8sgpt-ai/k8sgpt/issues/477)) ([075066d](https://github.com/k8sgpt-ai/k8sgpt/commit/075066dd7c353c0afd36637f421229cba5a6e022)), closes [#476](https://github.com/k8sgpt-ai/k8sgpt/issues/476) ### Docs * fix add localai command in readme.md ([#494](https://github.com/k8sgpt-ai/k8sgpt/issues/494)) ([40fbba7](https://github.com/k8sgpt-ai/k8sgpt/commit/40fbba7df1b2ce40c99262c901c7d2a26e9bbed0)) ### Other * customized prompt template for integration plugins ([#403](https://github.com/k8sgpt-ai/k8sgpt/issues/403)) ([c85203b](https://github.com/k8sgpt-ai/k8sgpt/commit/c85203bccde094c33ef83eb728aeed2608cbc136)) * **deps:** update actions/checkout digest to c85c95e ([#492](https://github.com/k8sgpt-ai/k8sgpt/issues/492)) ([1ae21e6](https://github.com/k8sgpt-ai/k8sgpt/commit/1ae21e6fd46b8490ea012fa8176d741af2e71e7e)) * **deps:** update docker/build-push-action digest to 44ea916 ([#491](https://github.com/k8sgpt-ai/k8sgpt/issues/491)) ([e556901](https://github.com/k8sgpt-ai/k8sgpt/commit/e556901b9d6205f75c819e1fbde51ba1f018e97d)) * **deps:** update docker/setup-buildx-action digest to 6a58db7 ([#489](https://github.com/k8sgpt-ai/k8sgpt/issues/489)) ([a23276d](https://github.com/k8sgpt-ai/k8sgpt/commit/a23276d3ff740abc6d3b36a4c793d90387ecee08)) * **deps:** update goreleaser/goreleaser-action digest to 336e299 ([#495](https://github.com/k8sgpt-ai/k8sgpt/issues/495)) ([ad2a5fd](https://github.com/k8sgpt-ai/k8sgpt/commit/ad2a5fd5fce78bf1bda4a48ad4a21598abadcdf4)) ## [0.3.6](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.5...v0.3.6) (2023-05-31) ### Features * get official field doc ([#457](https://github.com/k8sgpt-ai/k8sgpt/issues/457)) ([f9621af](https://github.com/k8sgpt-ai/k8sgpt/commit/f9621af7e480f490710020b931cbb08fb9824740)) ### Bug Fixes * **deps:** update module github.com/aws/aws-sdk-go to v1.44.270 ([#465](https://github.com/k8sgpt-ai/k8sgpt/issues/465)) ([5cf4fc5](https://github.com/k8sgpt-ai/k8sgpt/commit/5cf4fc52da4542a8bae98764d2fa7e337d95e5bd)) * **deps:** update module github.com/aws/aws-sdk-go to v1.44.271 ([#469](https://github.com/k8sgpt-ai/k8sgpt/issues/469)) ([1459dd4](https://github.com/k8sgpt-ai/k8sgpt/commit/1459dd4b8eca937e95ebe9b727311dc8b023e304)) * **deps:** update module github.com/aws/aws-sdk-go to v1.44.272 ([#473](https://github.com/k8sgpt-ai/k8sgpt/issues/473)) ([5233627](https://github.com/k8sgpt-ai/k8sgpt/commit/523362765f4c064c02798bb9e6f31e2bcc856e5f)) * **deps:** update module github.com/spf13/viper to v1.16.0 ([#472](https://github.com/k8sgpt-ai/k8sgpt/issues/472)) ([6052a5b](https://github.com/k8sgpt-ai/k8sgpt/commit/6052a5b4d77902e1882e3121b678671c89b57af8)) * **deps:** update module github.com/stretchr/testify to v1.8.4 ([#471](https://github.com/k8sgpt-ai/k8sgpt/issues/471)) ([42437f7](https://github.com/k8sgpt-ai/k8sgpt/commit/42437f77d1e0735a8f38a62ddbefb4d1f4e61c0e)) * name of sa reference in deployment ([#468](https://github.com/k8sgpt-ai/k8sgpt/issues/468)) ([cd049c9](https://github.com/k8sgpt-ai/k8sgpt/commit/cd049c9b4b188f702608d989fb32ae62f333dac5)) * typo ([#463](https://github.com/k8sgpt-ai/k8sgpt/issues/463)) ([1b86a6f](https://github.com/k8sgpt-ai/k8sgpt/commit/1b86a6fc89f90d29fdf2fab87a517f0da225ec96)) ### Other * **deps:** update google-github-actions/release-please-action digest to 51ee8ae ([#464](https://github.com/k8sgpt-ai/k8sgpt/issues/464)) ([86ebc23](https://github.com/k8sgpt-ai/k8sgpt/commit/86ebc23de762583b5904605f5651bbc83760aa95)) ## [0.3.5](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.4...v0.3.5) (2023-05-25) ### Features * add configuration api route ([#459](https://github.com/k8sgpt-ai/k8sgpt/issues/459)) ([fa4a075](https://github.com/k8sgpt-ai/k8sgpt/commit/fa4a0757b83f8ec00df951d49316f10961daa0e0)) ### Bug Fixes * **deps:** update module github.com/aws/aws-sdk-go to v1.44.267 ([#451](https://github.com/k8sgpt-ai/k8sgpt/issues/451)) ([49e120c](https://github.com/k8sgpt-ai/k8sgpt/commit/49e120c28e8b5ce5a8f7255ebc0f1b1b5c423f95)) * **deps:** update module github.com/aws/aws-sdk-go to v1.44.269 ([#458](https://github.com/k8sgpt-ai/k8sgpt/issues/458)) ([2994c1c](https://github.com/k8sgpt-ai/k8sgpt/commit/2994c1c5a77ce6ebe6e59d6edc9647c02f06f261)) * updated list.go to handle k8sgpt cache list crashing issue ([#455](https://github.com/k8sgpt-ai/k8sgpt/issues/455)) ([6eac58d](https://github.com/k8sgpt-ai/k8sgpt/commit/6eac58d4b03169356d3f06674ef206472e149fde)) ## [0.3.4](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.3...v0.3.4) (2023-05-22) ### Bug Fixes * **deps:** update module github.com/aws/aws-sdk-go to v1.44.266 ([#446](https://github.com/k8sgpt-ai/k8sgpt/issues/446)) ([edda743](https://github.com/k8sgpt-ai/k8sgpt/commit/edda743fa2bf4b5ae2551c981447a5912a459bb4)) * **deps:** update module github.com/stretchr/testify to v1.8.3 ([#442](https://github.com/k8sgpt-ai/k8sgpt/issues/442)) ([fe450eb](https://github.com/k8sgpt-ai/k8sgpt/commit/fe450eb69da0645328e60e2d7b0852ffdb996dee)) ### Other * add more filter releavent UT in analysis_test.go ([#435](https://github.com/k8sgpt-ai/k8sgpt/issues/435)) ([36995fd](https://github.com/k8sgpt-ai/k8sgpt/commit/36995fd4ed41c8c83ebc308f8ec2a4bfe530f7dc)) ## [0.3.3](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.2...v0.3.3) (2023-05-20) ### Features * caching ([#439](https://github.com/k8sgpt-ai/k8sgpt/issues/439)) ([948dae5](https://github.com/k8sgpt-ai/k8sgpt/commit/948dae5e288ec3bb0165eb3ce32171b12003f9c7)) * rework auth commands ([#438](https://github.com/k8sgpt-ai/k8sgpt/issues/438)) ([c659a87](https://github.com/k8sgpt-ai/k8sgpt/commit/c659a875fc296849a3703bfd7f0c4796f44bdf5a)) ### Bug Fixes * append coreAnalyzer if active_filter is empty and integration is added ([#441](https://github.com/k8sgpt-ai/k8sgpt/issues/441)) ([b7dc384](https://github.com/k8sgpt-ai/k8sgpt/commit/b7dc3845476759bedb3a55f77c8779e4a9f460dd)) * **deps:** update kubernetes packages to v0.27.2 ([#436](https://github.com/k8sgpt-ai/k8sgpt/issues/436)) ([d13b913](https://github.com/k8sgpt-ai/k8sgpt/commit/d13b91301cab5e05349b68716cd506fa1705f36f)) * **deps:** update module github.com/aws/aws-sdk-go to v1.44.265 ([#445](https://github.com/k8sgpt-ai/k8sgpt/issues/445)) ([c588e96](https://github.com/k8sgpt-ai/k8sgpt/commit/c588e963de3f238f07200d6cf09fe6f9781484f5)) * docker version ([#444](https://github.com/k8sgpt-ai/k8sgpt/issues/444)) ([1f767eb](https://github.com/k8sgpt-ai/k8sgpt/commit/1f767ebd2e31e61decab36218b1b85f2b3b6207d)) * use coreAnalyzer if there are no filters selected and no active_filters ([#432](https://github.com/k8sgpt-ai/k8sgpt/issues/432)) ([f0d3f36](https://github.com/k8sgpt-ai/k8sgpt/commit/f0d3f36f6d56bd76248590c0b841dffb7769a2ee)) ## [0.3.2](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.1...v0.3.2) (2023-05-16) ### Features * added the ability to set a user default AI provider ([#427](https://github.com/k8sgpt-ai/k8sgpt/issues/427)) ([cbe2fb4](https://github.com/k8sgpt-ai/k8sgpt/commit/cbe2fb4a4c160a0a24b3fb4602cae8e5eebd6aa0)) ### Bug Fixes * improve default_prompt ([#406](https://github.com/k8sgpt-ai/k8sgpt/issues/406)) ([06542b4](https://github.com/k8sgpt-ai/k8sgpt/commit/06542b4bf1aec193f11a40526a1b60be01972e66)) * missing validation for backend option in remove command ([#429](https://github.com/k8sgpt-ai/k8sgpt/issues/429)) ([af826d5](https://github.com/k8sgpt-ai/k8sgpt/commit/af826d500fef0469b958250161b0827aa4ab2933)) ### Other * **deps:** bump github.com/docker/distribution ([#428](https://github.com/k8sgpt-ai/k8sgpt/issues/428)) ([3099909](https://github.com/k8sgpt-ai/k8sgpt/commit/30999091136c64173e5c15b789036c85f8b855f3)) * **deps:** update actions/setup-go digest to fac708d ([#422](https://github.com/k8sgpt-ai/k8sgpt/issues/422)) ([097c791](https://github.com/k8sgpt-ai/k8sgpt/commit/097c7912b0572d0461f08af12e9f21c6618c13e4)) * **deps:** update reviewdog/action-golangci-lint digest to 79d32f1 ([#425](https://github.com/k8sgpt-ai/k8sgpt/issues/425)) ([032576c](https://github.com/k8sgpt-ai/k8sgpt/commit/032576c728751522fe6cd8f255366cc3d5c0f23c)) ## [0.3.1](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.0...v0.3.1) (2023-05-15) ### Features * add error message if analyze request fail ([#393](https://github.com/k8sgpt-ai/k8sgpt/issues/393)) ([639aa12](https://github.com/k8sgpt-ai/k8sgpt/commit/639aa12931b3fc9f99a4b34f33f583b9f427f40c)) * add remove command to remove a backend AI provider ([#395](https://github.com/k8sgpt-ai/k8sgpt/issues/395)) ([c5b72ee](https://github.com/k8sgpt-ai/k8sgpt/commit/c5b72eee165a29d4ed59b0ec2f19e9f58267840d)) * filters api ([#407](https://github.com/k8sgpt-ai/k8sgpt/issues/407)) ([e5e613a](https://github.com/k8sgpt-ai/k8sgpt/commit/e5e613acee0a99f108dd90affc55a18dee42ad9c)) * use correct port to metrics ([#390](https://github.com/k8sgpt-ai/k8sgpt/issues/390)) ([5d4e591](https://github.com/k8sgpt-ai/k8sgpt/commit/5d4e591f11e555cac851205fff158ef22f702c33)) ### Bug Fixes * clusterole name ([#392](https://github.com/k8sgpt-ai/k8sgpt/issues/392)) ([123b8a6](https://github.com/k8sgpt-ai/k8sgpt/commit/123b8a66eed1af41b7bd4e558ba4f0f8ef947e98)) * **deps:** update module buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go to v1.3.0-20230515081240-6b5b845c638e.1 ([#397](https://github.com/k8sgpt-ai/k8sgpt/issues/397)) ([a1f98ad](https://github.com/k8sgpt-ai/k8sgpt/commit/a1f98ad78ecd9a84d26e7a59c340e5410524e561)) * **deps:** update module buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go to v1.28.1-20230510140658-54288a50e81c.4 ([#398](https://github.com/k8sgpt-ai/k8sgpt/issues/398)) ([50916f2](https://github.com/k8sgpt-ai/k8sgpt/commit/50916f2c93fb19b935f835eda1ba78b7c0465fe6)) * **deps:** update module google.golang.org/grpc to v1.55.0 ([#389](https://github.com/k8sgpt-ai/k8sgpt/issues/389)) ([8cfb717](https://github.com/k8sgpt-ai/k8sgpt/commit/8cfb717dc15a6af489a16282d38d3495518bc919)) * **deps:** update module helm.sh/helm/v3 to v3.12.0 ([#396](https://github.com/k8sgpt-ai/k8sgpt/issues/396)) ([c1410d1](https://github.com/k8sgpt-ai/k8sgpt/commit/c1410d169945341e9a635e12c2adcc87a08f8a09)) * update engine's cmd flag to match the new cli layout ([#400](https://github.com/k8sgpt-ai/k8sgpt/issues/400)) ([aafe669](https://github.com/k8sgpt-ai/k8sgpt/commit/aafe669739aa8c38611d13deb08706096c7893e0)) ### Other * gofmt fix and enable in CI ([#414](https://github.com/k8sgpt-ai/k8sgpt/issues/414)) ([e66de8c](https://github.com/k8sgpt-ai/k8sgpt/commit/e66de8c4cea1213cda1db609f07cbb0c8f6591c3)) * make go-lint happy ([#405](https://github.com/k8sgpt-ai/k8sgpt/issues/405)) ([ed73485](https://github.com/k8sgpt-ai/k8sgpt/commit/ed73485d92af0329915633d51c7eccdbce9b37cc)) ## [0.3.0](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.2.9...v0.3.0) (2023-05-09) ### ⚠ BREAKING CHANGES * migrate api to grpc ([#386](https://github.com/k8sgpt-ai/k8sgpt/issues/386)) ### Features * add auth commands ([#369](https://github.com/k8sgpt-ai/k8sgpt/issues/369)) ([00aaae8](https://github.com/k8sgpt-ai/k8sgpt/commit/00aaae86d88812bd6b6be290ba440ea583ccc01c)) * migrate api to grpc ([#386](https://github.com/k8sgpt-ai/k8sgpt/issues/386)) ([9998e76](https://github.com/k8sgpt-ai/k8sgpt/commit/9998e7620d2803b82b241482649449507040add3)) ### Bug Fixes * **deps:** update module github.com/sashabaranov/go-openai to v1.9.3 ([#378](https://github.com/k8sgpt-ai/k8sgpt/issues/378)) ([045a063](https://github.com/k8sgpt-ai/k8sgpt/commit/045a06350bf41d4177e67316978af8fcf02ff19a)) * **deps:** update module golang.org/x/term to v0.8.0 ([#382](https://github.com/k8sgpt-ai/k8sgpt/issues/382)) ([65fff11](https://github.com/k8sgpt-ai/k8sgpt/commit/65fff11e585f8074fb77124b25339a09da313970)) ### Docs * update README ([#383](https://github.com/k8sgpt-ai/k8sgpt/issues/383)) ([d6bcb96](https://github.com/k8sgpt-ai/k8sgpt/commit/d6bcb96105a549eb772b790704c7ec27e374eafd)) ### Other * **deps:** update anchore/sbom-action action to v0.14.2 ([#387](https://github.com/k8sgpt-ai/k8sgpt/issues/387)) ([9192b26](https://github.com/k8sgpt-ai/k8sgpt/commit/9192b26fab2ce09c8a480256a15723ae788612c3)) * fix the logo URL ([#384](https://github.com/k8sgpt-ai/k8sgpt/issues/384)) ([b6b0612](https://github.com/k8sgpt-ai/k8sgpt/commit/b6b06123db8914cae09dfa96edd92aff83823bdf)) ## [0.2.9](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.2.8...v0.2.9) (2023-05-03) ### Features * add additionalLabels to Service Monitor ([#366](https://github.com/k8sgpt-ai/k8sgpt/issues/366)) ([a89a5cf](https://github.com/k8sgpt-ai/k8sgpt/commit/a89a5cfa2efd365cfc1c501c7055a315949c5f97)) * add azure openai provider ([#309](https://github.com/k8sgpt-ai/k8sgpt/issues/309)) ([d8357ce](https://github.com/k8sgpt-ai/k8sgpt/commit/d8357ceb949e04d9dd21276a1d1dfcb60010c37a)) * add helm chart ([#318](https://github.com/k8sgpt-ai/k8sgpt/issues/318)) ([5af8178](https://github.com/k8sgpt-ai/k8sgpt/commit/5af817880278771cf0b25d0936b90a45c089218c)) * improving readme ([7471b76](https://github.com/k8sgpt-ai/k8sgpt/commit/7471b7679469bc7416ee35061a13e8442bfc532c)) * rework output format ([#368](https://github.com/k8sgpt-ai/k8sgpt/issues/368)) ([8b49f70](https://github.com/k8sgpt-ai/k8sgpt/commit/8b49f708f364569994801757cf72982291c0de82)) ### Bug Fixes * **deps:** update module github.com/aquasecurity/trivy-operator to v0.13.2 ([#353](https://github.com/k8sgpt-ai/k8sgpt/issues/353)) ([363294c](https://github.com/k8sgpt-ai/k8sgpt/commit/363294c310ca1a6666d4d2dcba7e232387f0d41f)) * **deps:** update module github.com/prometheus/client_golang to v1.15.1 ([#374](https://github.com/k8sgpt-ai/k8sgpt/issues/374)) ([799869b](https://github.com/k8sgpt-ai/k8sgpt/commit/799869bcef76be0d5b2169646de106bd3b441c50)) * **deps:** update module github.com/sashabaranov/go-openai to v1.9.1 ([#363](https://github.com/k8sgpt-ai/k8sgpt/issues/363)) ([766a15b](https://github.com/k8sgpt-ai/k8sgpt/commit/766a15b25a2669b101c717013dae0ea4f5d0daa3)) * **deps:** update module github.com/sashabaranov/go-openai to v1.9.2 ([#375](https://github.com/k8sgpt-ai/k8sgpt/issues/375)) ([8b82d59](https://github.com/k8sgpt-ai/k8sgpt/commit/8b82d59bd7a6837a543467f9d76c66e70f267b85)) * update CONTRIBUTING.md Slack URL ([#373](https://github.com/k8sgpt-ai/k8sgpt/issues/373)) ([e0200e7](https://github.com/k8sgpt-ai/k8sgpt/commit/e0200e7fa05597b52c9f25fc2e9b744aea764b9a)) ### Docs * remove issue templates to use org wide ones ([#352](https://github.com/k8sgpt-ai/k8sgpt/issues/352)) ([3051b1c](https://github.com/k8sgpt-ai/k8sgpt/commit/3051b1ca343d739e48ff05c96468040adefc929a)) * update README.md ([#356](https://github.com/k8sgpt-ai/k8sgpt/issues/356)) ([4e146fb](https://github.com/k8sgpt-ai/k8sgpt/commit/4e146fb152b79a43f3163a94389022b4325126d4)) ### Other * added changing banners ([#367](https://github.com/k8sgpt-ai/k8sgpt/issues/367)) ([4f6e833](https://github.com/k8sgpt-ai/k8sgpt/commit/4f6e833d3427adf82438186f52eee40293c50cd0)) * **deps:** update golang docker tag to v1.20.4 ([#370](https://github.com/k8sgpt-ai/k8sgpt/issues/370)) ([9faa694](https://github.com/k8sgpt-ai/k8sgpt/commit/9faa69422dab812ec062c10a8fffb73160a3cd21)) * **deps:** update reviewdog/action-golangci-lint digest to f5d8591 ([#362](https://github.com/k8sgpt-ai/k8sgpt/issues/362)) ([b8a5f3b](https://github.com/k8sgpt-ai/k8sgpt/commit/b8a5f3bab85e28f83259739958fe22ca0ada211a)) * updated logo ([#365](https://github.com/k8sgpt-ai/k8sgpt/issues/365)) ([6431be7](https://github.com/k8sgpt-ai/k8sgpt/commit/6431be7771bd7c34d6beac14470bc6918b1793c4)) ## [0.2.8](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.2.7...v0.2.8) (2023-04-27) ### Features * don't ask for password if backend is localai ([74d9a75](https://github.com/k8sgpt-ai/k8sgpt/commit/74d9a750ca01361eb81fdcc91eb5886ecff1d17c)) * introduce linter to run on PR builds ([#333](https://github.com/k8sgpt-ai/k8sgpt/issues/333)) ([252c734](https://github.com/k8sgpt-ai/k8sgpt/commit/252c7343106bf64c86861a9452e8618efc72881c)), closes [#330](https://github.com/k8sgpt-ai/k8sgpt/issues/330) ### Bug Fixes * remove dead code ([c29860d](https://github.com/k8sgpt-ai/k8sgpt/commit/c29860d418faa316bc167721e443f7b64eafd970)) * report failure if network policy doesn't match any pods ([8adde6b](https://github.com/k8sgpt-ai/k8sgpt/commit/8adde6bf873b46f365146bc14fc4c8f46d82f8dc)) * take `KUBECONFIG` env variable into consideration ([#340](https://github.com/k8sgpt-ai/k8sgpt/issues/340)) ([ee85d13](https://github.com/k8sgpt-ai/k8sgpt/commit/ee85d13d59e045519b087adaf55520acc2c205db)), closes [#331](https://github.com/k8sgpt-ai/k8sgpt/issues/331) * use a cache file name with a fixed size. ([#350](https://github.com/k8sgpt-ai/k8sgpt/issues/350)) ([dee4235](https://github.com/k8sgpt-ai/k8sgpt/commit/dee423519eb35f11c3e3a6dd64981e781899fe22)) * use correct result slice for cronjob analyzer ([947e94f](https://github.com/k8sgpt-ai/k8sgpt/commit/947e94f35379712a2fb1e2a2c90636606e0e44b6)) ### Docs * fix README ([#345](https://github.com/k8sgpt-ai/k8sgpt/issues/345)) ([f8fa35c](https://github.com/k8sgpt-ai/k8sgpt/commit/f8fa35cf9d591691679d6881fcc203e3411d99aa)) ### Other * add settings ([#351](https://github.com/k8sgpt-ai/k8sgpt/issues/351)) ([3af3667](https://github.com/k8sgpt-ai/k8sgpt/commit/3af366788fb47ff87be0142446c027f5a90491e7)) * **deps:** pin dependencies ([#336](https://github.com/k8sgpt-ai/k8sgpt/issues/336)) ([125341b](https://github.com/k8sgpt-ai/k8sgpt/commit/125341bdaacbc8bedbb333e498dabfb5c72a24c0)) * logo update ([#339](https://github.com/k8sgpt-ai/k8sgpt/issues/339)) ([d4dcc7a](https://github.com/k8sgpt-ai/k8sgpt/commit/d4dcc7a3991a861923c8115c0c82759b9e83bcfa)) * update Apache2 license ([#342](https://github.com/k8sgpt-ai/k8sgpt/issues/342)) ([aca5806](https://github.com/k8sgpt-ai/k8sgpt/commit/aca58064c36b3bc13699e055a7cca8a493320078)) * update README.md ([#346](https://github.com/k8sgpt-ai/k8sgpt/issues/346)) ([14a3537](https://github.com/k8sgpt-ai/k8sgpt/commit/14a3537ce9bc9d581b78329be899a66bc14db648)) * updated banner ([#343](https://github.com/k8sgpt-ai/k8sgpt/issues/343)) ([0995e00](https://github.com/k8sgpt-ai/k8sgpt/commit/0995e008fe64f5978c3a0cc9fb4c525470f00dfa)) ## [0.2.7](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.2.6...v0.2.7) (2023-04-25) ### Bug Fixes * remove pointer to loop variable when searching the latest event to analyze ([#328](https://github.com/k8sgpt-ai/k8sgpt/issues/328)) ([2616220](https://github.com/k8sgpt-ai/k8sgpt/commit/2616220935d450030c8a9f2f2741c3607aa4b663)) ## [0.2.6](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.2.5...v0.2.6) (2023-04-25) ### Bug Fixes * explicitly pass in filter to async analysis go routine ([#326](https://github.com/k8sgpt-ai/k8sgpt/issues/326)) ([692cd06](https://github.com/k8sgpt-ai/k8sgpt/commit/692cd06c385c1c6f458994f6e975a9fce2bc1c57)) ## [0.2.5](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.2.4...v0.2.5) (2023-04-25) ### Features * add configuration interface to support customer providers ([84a3cc0](https://github.com/k8sgpt-ai/k8sgpt/commit/84a3cc05fb6e21b732ef777351b42db8045e1093)) * add k8sgpt grafana dashboard ([#316](https://github.com/k8sgpt-ai/k8sgpt/issues/316)) ([ff79982](https://github.com/k8sgpt-ai/k8sgpt/commit/ff799825cfe5856bb97c8f38d939ec36b19fa30a)) * add serve & integration to README ([a65ee7f](https://github.com/k8sgpt-ai/k8sgpt/commit/a65ee7fc0957c7ba9369bdbe12e648818ca3f841)) * add subproject group to CODEOWNERS ([#322](https://github.com/k8sgpt-ai/k8sgpt/issues/322)) ([2391603](https://github.com/k8sgpt-ai/k8sgpt/commit/2391603075e73b91d9988d40eecddfc3e0593405)) * allow to set a baseurl ([#310](https://github.com/k8sgpt-ai/k8sgpt/issues/310)) ([cf797a6](https://github.com/k8sgpt-ai/k8sgpt/commit/cf797a6eb67efba957704077b4b04ed3ee166c24)) * async calls ([#311](https://github.com/k8sgpt-ai/k8sgpt/issues/311)) ([c3cc413](https://github.com/k8sgpt-ai/k8sgpt/commit/c3cc413e7fc3b06b310779dfa3cb4863ea9f3ed2)) * modify error handling to return a list of errors to display to the user at the end of analysis without blocking it if an error is detected (e.g., version of an object is not available on user's cluster) ([fa087ff](https://github.com/k8sgpt-ai/k8sgpt/commit/fa087ff5593871d2a07d68f203dd91e66c57e40b)) * the overall optimization and architecture design of the makefile are made ([#317](https://github.com/k8sgpt-ai/k8sgpt/issues/317)) ([754bf91](https://github.com/k8sgpt-ai/k8sgpt/commit/754bf917e1ac524699d38fb2dc59bc5d858f6d80)) * update readme ([#314](https://github.com/k8sgpt-ai/k8sgpt/issues/314)) ([ddd830c](https://github.com/k8sgpt-ai/k8sgpt/commit/ddd830cc569278c157480c44a671c9be20c95b24)) * use OS conform path for storing cached results ([7eddb8f](https://github.com/k8sgpt-ai/k8sgpt/commit/7eddb8f4a6dc61d5f66fc1bf56c0e8cbf9370229)), closes [#323](https://github.com/k8sgpt-ai/k8sgpt/issues/323) ### Bug Fixes * **deps:** update module github.com/aquasecurity/trivy-operator to v0.13.1 ([#321](https://github.com/k8sgpt-ai/k8sgpt/issues/321)) ([e7f74db](https://github.com/k8sgpt-ai/k8sgpt/commit/e7f74db6e556146b898437bb777c2b803d1bec4f)) * **deps:** update module github.com/prometheus/client_golang to v1.15.0 ([#303](https://github.com/k8sgpt-ai/k8sgpt/issues/303)) ([df2ed41](https://github.com/k8sgpt-ai/k8sgpt/commit/df2ed4185b5a33a18e6b144c85bec3902c14d209)) * **deps:** update module github.com/sashabaranov/go-openai to v1.9.0 ([#298](https://github.com/k8sgpt-ai/k8sgpt/issues/298)) ([0472c36](https://github.com/k8sgpt-ai/k8sgpt/commit/0472c363a4d8a90556bc744fbf513ad63281e38b)) ### Other * add serviceMonitor in sample yaml ([#304](https://github.com/k8sgpt-ai/k8sgpt/issues/304)) ([0a4ed0d](https://github.com/k8sgpt-ai/k8sgpt/commit/0a4ed0d907c22a924dd79e8945eb9d6d10cd9ce7)) * analyze Pod ReadinessProbe faliure ([3c7e0bb](https://github.com/k8sgpt-ai/k8sgpt/commit/3c7e0bba1d4cc8247d248756dcfef884bc406992)) * change license to Apache-2 ([#313](https://github.com/k8sgpt-ai/k8sgpt/issues/313)) ([d0f7a11](https://github.com/k8sgpt-ai/k8sgpt/commit/d0f7a1105fe7ed317785782d3af45c83766b7d80)) ## [0.2.4](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.2.3...v0.2.4) (2023-04-18) ### Features * improve HPA analyzer to check ScaleTargetRef resources ([#283](https://github.com/k8sgpt-ai/k8sgpt/issues/283)) ([7173203](https://github.com/k8sgpt-ai/k8sgpt/commit/71732037fa40071cef0c2bc143736019d75eac86)) * init logging middleware on server mode ([6742410](https://github.com/k8sgpt-ai/k8sgpt/commit/6742410025d5e99c60045bb314730799f0e1e5ce)) ### Bug Fixes * deployment/cronjob namespace filtering ([#290](https://github.com/k8sgpt-ai/k8sgpt/issues/290)) ([3d684a2](https://github.com/k8sgpt-ai/k8sgpt/commit/3d684a2af7a9e1821bdb8b1bd6e85867b800d3ee)) * ensure parent directories are created in EnsureDirExists function ([#293](https://github.com/k8sgpt-ai/k8sgpt/issues/293)) ([af8b350](https://github.com/k8sgpt-ai/k8sgpt/commit/af8b350520d1a187a199482dd338db0086118db8)) * resolve language toggle bug (issue [#294](https://github.com/k8sgpt-ai/k8sgpt/issues/294)) ([0313627](https://github.com/k8sgpt-ai/k8sgpt/commit/03136278486ba12e3352580b317b9e63fa3a80f0)) ## [0.2.3](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.2.2...v0.2.3) (2023-04-16) ### Features * add node analyzer ([#272](https://github.com/k8sgpt-ai/k8sgpt/issues/272)) ([6247a1c](https://github.com/k8sgpt-ai/k8sgpt/commit/6247a1c0f3c2ead6a59661afed06973c29e57eca)) * add output query param on serve mode & refactor output logic ([9642202](https://github.com/k8sgpt-ai/k8sgpt/commit/9642202ed1b09c06a687651b7818c2a4df8a0c06)) * add server metrics ([#273](https://github.com/k8sgpt-ai/k8sgpt/issues/273)) ([a3becc9](https://github.com/k8sgpt-ai/k8sgpt/commit/a3becc9906515d0567808fee9a4e322451d6dc3f)) * envs to initialise server ([0071e25](https://github.com/k8sgpt-ai/k8sgpt/commit/0071e25992fc86c3882c2066873a2b04b43fe476)) * rename server/main.go to server/server.go ([9121a98](https://github.com/k8sgpt-ai/k8sgpt/commit/9121a983e52fa15c07bcc3bb361df97b8085c24c)) * running in cluster ([842f08c](https://github.com/k8sgpt-ai/k8sgpt/commit/842f08c655fde66b6b628192490e50be2ac3dcef)) * running in cluster ([3988eb2](https://github.com/k8sgpt-ai/k8sgpt/commit/3988eb2fd0a7d29ffa7b7bbc59960ca91e50466e)) * switch config file to XDG conform location ([dee4355](https://github.com/k8sgpt-ai/k8sgpt/commit/dee435514d7f717e4eb63b15a9d9fdb0722330ac)) * wip blocked until we have envs ([fe2c08c](https://github.com/k8sgpt-ai/k8sgpt/commit/fe2c08cf72a6ca271d1b431be66653f1396f304d)) ### Bug Fixes * add new line after version cmd output ([92e7b3d](https://github.com/k8sgpt-ai/k8sgpt/commit/92e7b3d3fb00c33ac48230caac34f45729e2f6b2)) * **deps:** update module github.com/sashabaranov/go-openai to v1.8.0 ([#277](https://github.com/k8sgpt-ai/k8sgpt/issues/277)) ([51b1b35](https://github.com/k8sgpt-ai/k8sgpt/commit/51b1b352acd24ebdc4cf9d9121f25c90e8f76ba7)) * resolve issue with duplicated integration filters. ([960ba56](https://github.com/k8sgpt-ai/k8sgpt/commit/960ba568d0dcc2ace722dc5c9b7c846366a98070)) * use the aiProvider object when launching the server instead of the deprecated configuration keys ([e7076ed](https://github.com/k8sgpt-ai/k8sgpt/commit/e7076ed6093aa9609d8c884b7a03e295057aaa8e)) ### Other * updated ([f0a0c9a](https://github.com/k8sgpt-ai/k8sgpt/commit/f0a0c9aebf627d65b0192ba3d0786cefd81e1fef)) ## [0.2.2](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.2.1...v0.2.2) (2023-04-14) ### Features * add simple health endpoint ([26c0cb2](https://github.com/k8sgpt-ai/k8sgpt/commit/26c0cb2eed75695220007e6d6f7b492c2641a149)) * anoymization based on pr feedback ([19e1b94](https://github.com/k8sgpt-ai/k8sgpt/commit/19e1b94e7c9ce4092f1dabd659023a193b2c4a92)) * anoymization based on pr feedback ([fe52951](https://github.com/k8sgpt-ai/k8sgpt/commit/fe529510b68ac5fbd39c147c7719abe2e7d20894)) * check for auth only in case of --explain ([57790e5](https://github.com/k8sgpt-ai/k8sgpt/commit/57790e5bc7037f57a4f73248fe05cac192511470)) * first version of serve ([b2e8add](https://github.com/k8sgpt-ai/k8sgpt/commit/b2e8adda333fbd508f0f01f2afcabc57bf9948c2)) * unified cmd and api ([9157d4d](https://github.com/k8sgpt-ai/k8sgpt/commit/9157d4dd1312bf75b336beb0e097422b303d22f1)) * updated api ([adae2ef](https://github.com/k8sgpt-ai/k8sgpt/commit/adae2ef71d81431711c552159362336e496b21ee)) ### Bug Fixes * bool conversion ([336ec2a](https://github.com/k8sgpt-ai/k8sgpt/commit/336ec2a42693d0df325b95cbebd9545b19e27725)) * **deps:** update module helm.sh/helm/v3 to v3.11.3 ([4dd91ed](https://github.com/k8sgpt-ai/k8sgpt/commit/4dd91ed8263292476054bc70d3d6a3149f88f1b3)) * naming ([159b385](https://github.com/k8sgpt-ai/k8sgpt/commit/159b3851ec54e93a447b0f13aa4ceb7b8b8f62db)) * start message ([6b63027](https://github.com/k8sgpt-ai/k8sgpt/commit/6b630275eb64b799c50e3074cb22a3b41bb893de)) ### Docs * fix Slack link ([1dccaea](https://github.com/k8sgpt-ai/k8sgpt/commit/1dccaea3f4f96b2da52999eed5031f02a89c0b6e)) ### Other * added oidc ([bffad41](https://github.com/k8sgpt-ai/k8sgpt/commit/bffad41134d231b16f136a619174ff3bee61765a)) * additional analyzers ([23071fd](https://github.com/k8sgpt-ai/k8sgpt/commit/23071fd2e6b421f0f5fcd6e7e4985c6900e5405c)) * **deps:** bump github.com/docker/docker ([#268](https://github.com/k8sgpt-ai/k8sgpt/issues/268)) ([7d1e2ac](https://github.com/k8sgpt-ai/k8sgpt/commit/7d1e2acaf3eaf00929ff43b9373df6a4be100795)) * **deps:** update actions/checkout digest to 83b7061 ([cbe6f27](https://github.com/k8sgpt-ai/k8sgpt/commit/cbe6f27c05e82f55f41b648b01972ba2c43f1534)) * **deps:** update actions/checkout digest to 8e5e7e5 ([#266](https://github.com/k8sgpt-ai/k8sgpt/issues/266)) ([0af34a1](https://github.com/k8sgpt-ai/k8sgpt/commit/0af34a1a95502dc26d7e08bac896f691e4969090)) * **deps:** update module oras.land/oras-go to v1.2.3 ([#249](https://github.com/k8sgpt-ai/k8sgpt/issues/249)) ([13c9231](https://github.com/k8sgpt-ai/k8sgpt/commit/13c9231aafef3a259fd678a80063ad2e968d6e95)) * fixing up tests ([f9b25d9](https://github.com/k8sgpt-ai/k8sgpt/commit/f9b25d9e85a8faaf1aae59d7bedc4c0f3538181e)) * fixing up tests ([498d454](https://github.com/k8sgpt-ai/k8sgpt/commit/498d454c174c7d39da1ca63b2a201e797d7e5e1c)) * Merge branch 'main' into feat/additional-analyzers ([4d36248](https://github.com/k8sgpt-ai/k8sgpt/commit/4d3624830ff840f9ccf11d7da20953bdf4c7c7fc)) * removing field ([ddb51c7](https://github.com/k8sgpt-ai/k8sgpt/commit/ddb51c7af470044a8514ed013b44cc135e4c0f10)) ## [0.2.1](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.2.0...v0.2.1) (2023-04-12) ### Features * add anonymization example to README ([8a60b57](https://github.com/k8sgpt-ai/k8sgpt/commit/8a60b579409c67f092156ba1adf1be22cce37b8c)) * add anonymization flag ([d2a84ea](https://github.com/k8sgpt-ai/k8sgpt/commit/d2a84ea2b5c800dd900aac3a48b1914bd9ddb917)) * add more details on anonymize flag ([b687473](https://github.com/k8sgpt-ai/k8sgpt/commit/b687473e6169406002b0ee8be6ebb9ce43b46495)) * add storage class names' check. ([c8ba7d6](https://github.com/k8sgpt-ai/k8sgpt/commit/c8ba7d62d2f1d262263d1dff8f980e91cdcd50e8)) * improve documentation ([6f08654](https://github.com/k8sgpt-ai/k8sgpt/commit/6f0865413fc2854450d217225199cec199972490)) * improve documentation & update hpa message ([11326c1](https://github.com/k8sgpt-ai/k8sgpt/commit/11326c1c5f307c718e8d1e56099537314ffedadd)) * improve security of the MaskString function ([08f2a89](https://github.com/k8sgpt-ai/k8sgpt/commit/08f2a89e54a65544322814286977b2c05acce89d)) * initial impl of integration ([b0e5170](https://github.com/k8sgpt-ai/k8sgpt/commit/b0e517006e65ac2b4e2d4e2696531d4bbf62c34b)) * initial impl of integration ([61d6e52](https://github.com/k8sgpt-ai/k8sgpt/commit/61d6e524657272cf3a967c724f212677fcfe7d2b)) * integration ready for first review ([3682f5c](https://github.com/k8sgpt-ai/k8sgpt/commit/3682f5c7ebb9590e92162eed214a8127f71bcd81)) * introduce StatefulSet analyser. ([c041ce2](https://github.com/k8sgpt-ai/k8sgpt/commit/c041ce2bbb4ecbc6f5637207c9f3071eee022744)) * refactor integration to use Failure object ([c0afc0f](https://github.com/k8sgpt-ai/k8sgpt/commit/c0afc0f5c91cfa50b1f7af901800ff0a2b492d18)) * return errors if filter specified by flag does not exist. ([dd5824f](https://github.com/k8sgpt-ai/k8sgpt/commit/dd5824f4365b01e3c501d8b5cda914dff138e03d)) ### Bug Fixes * **deps:** update kubernetes packages to v0.27.0 ([7a97034](https://github.com/k8sgpt-ai/k8sgpt/commit/7a97034cf41cb265111c752ee3d54fd90524ef59)) * **deps:** update module github.com/sashabaranov/go-openai to v1.7.0 ([#227](https://github.com/k8sgpt-ai/k8sgpt/issues/227)) ([5f3a5a5](https://github.com/k8sgpt-ai/k8sgpt/commit/5f3a5a54a02967acce40f8b4e9dd3a154c83f58c)) * exit progressbar on error ([#99](https://github.com/k8sgpt-ai/k8sgpt/issues/99)) ([fe261b3](https://github.com/k8sgpt-ai/k8sgpt/commit/fe261b375f4d7990906620f53ac26e792a34731b)) * exit progressbar on error ([#99](https://github.com/k8sgpt-ai/k8sgpt/issues/99)) ([ab55f15](https://github.com/k8sgpt-ai/k8sgpt/commit/ab55f157ef026502d29eadf5ad83e917fe085a6c)) * improve ReplaceIfMatch regex ([fd936ce](https://github.com/k8sgpt-ai/k8sgpt/commit/fd936ceaf725d1c1ed1f53eaa2204455dcd1e2af)) * pdb test ([705d2a0](https://github.com/k8sgpt-ai/k8sgpt/commit/705d2a0dcebb63783782e06b6b775393daf1efb7)) * use hpa namespace instead analyzer namespace ([#230](https://github.com/k8sgpt-ai/k8sgpt/issues/230)) ([a582d44](https://github.com/k8sgpt-ai/k8sgpt/commit/a582d444c5c53f25d7172947c690b35cad2cc176)) ### Docs * add statefulSet analyzer in the docs. ([#233](https://github.com/k8sgpt-ai/k8sgpt/issues/233)) ([b45ff1a](https://github.com/k8sgpt-ai/k8sgpt/commit/b45ff1aa8ef447df2b74bb8c6225e2f3d7c5bd63)) * add statefulSet analyzer in the docs. ([#233](https://github.com/k8sgpt-ai/k8sgpt/issues/233)) ([ba01bd4](https://github.com/k8sgpt-ai/k8sgpt/commit/ba01bd4b6ecd64fbe249be54f20471afc6339208)) ### Other * add fakeai provider ([#218](https://github.com/k8sgpt-ai/k8sgpt/issues/218)) ([e449cb6](https://github.com/k8sgpt-ai/k8sgpt/commit/e449cb60230d440d5b8e00062db63de5d6d413bf)) * adding k8sgpt-approvers ([#238](https://github.com/k8sgpt-ai/k8sgpt/issues/238)) ([db1388f](https://github.com/k8sgpt-ai/k8sgpt/commit/db1388fd20dcf21069adcecd2796f2e1231162c8)) * adding k8sgpt-approvers ([#238](https://github.com/k8sgpt-ai/k8sgpt/issues/238)) ([992b107](https://github.com/k8sgpt-ai/k8sgpt/commit/992b107c2d906663bb22998004a0859bccd45c77)) * compiling successfully ([80ac51c](https://github.com/k8sgpt-ai/k8sgpt/commit/80ac51c804351226e1764e3e649ac56e22de3749)) * **deps:** update anchore/sbom-action action to v0.14.1 ([#228](https://github.com/k8sgpt-ai/k8sgpt/issues/228)) ([9423b53](https://github.com/k8sgpt-ai/k8sgpt/commit/9423b53c1dbae3d0762420a0bacbdace9a2c18c9)) * **deps:** update google-github-actions/release-please-action digest to c078ea3 ([a1d8012](https://github.com/k8sgpt-ai/k8sgpt/commit/a1d8012a5c748aee3f16621d6da9a0f0c8cba293)) * **deps:** update google-github-actions/release-please-action digest to f7edb9e ([#241](https://github.com/k8sgpt-ai/k8sgpt/issues/241)) ([55dda43](https://github.com/k8sgpt-ai/k8sgpt/commit/55dda432ab89c4917bd28fceabcbe5569c0bf530)) * **deps:** update google-github-actions/release-please-action digest to f7edb9e ([#241](https://github.com/k8sgpt-ai/k8sgpt/issues/241)) ([21dc61c](https://github.com/k8sgpt-ai/k8sgpt/commit/21dc61c04f4d772b5147b38a4d28e5dbddf5cdd8)) * fix mistake introduced by ab55f157 ([#240](https://github.com/k8sgpt-ai/k8sgpt/issues/240)) ([428c348](https://github.com/k8sgpt-ai/k8sgpt/commit/428c3485868a7be95ea6776694e30b36badf4b5c)) * fix mistake introduced by ab55f157 ([#240](https://github.com/k8sgpt-ai/k8sgpt/issues/240)) ([3845d47](https://github.com/k8sgpt-ai/k8sgpt/commit/3845d4747f4e0fc823d1bcf631d6ecdd5e4ccd03)) * Fixing broken tests ([c809af3](https://github.com/k8sgpt-ai/k8sgpt/commit/c809af3f47388599fda3a88a4638feae1dc90492)) * fixing filters ([258c69a](https://github.com/k8sgpt-ai/k8sgpt/commit/258c69a17c977867dfd0a7ad02727270b7c172e7)) * fixing filters ([4d20f70](https://github.com/k8sgpt-ai/k8sgpt/commit/4d20f70fb40ff326ceb279f699068ec4956a2f10)) * merged ([096321b](https://github.com/k8sgpt-ai/k8sgpt/commit/096321b31a6cf0d53b1861a3e4ad1efe84f697cc)) * updated analysis_test.go ([825e9a4](https://github.com/k8sgpt-ai/k8sgpt/commit/825e9a43bd3ab7aa3ea52f315993cd778ea039e3)) * updated link output ([1b7f4ce](https://github.com/k8sgpt-ai/k8sgpt/commit/1b7f4ce44a499e5389aec42fdee00bfa81ef0888)) * updating based on feedback ([5e5d4b6](https://github.com/k8sgpt-ai/k8sgpt/commit/5e5d4b6de160dc7533067e1c0d8403c3faac1a9f)) * weird new line after filter removed ([fabe01a](https://github.com/k8sgpt-ai/k8sgpt/commit/fabe01aa019f1db45ed2ff780f0d6d63297b230b)) ## [0.2.0](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.1.8...v0.2.0) (2023-04-05) ### ⚠ BREAKING CHANGES * The format of the configuration file has changed. Users must update their configuration files to use the new format. ### Features * add support for new configuration format ([9b243cd](https://github.com/k8sgpt-ai/k8sgpt/commit/9b243cdcaab1742fca2516bc1ae5710505e0eb65)) * add tests for hpa analyzer ([5a59abb](https://github.com/k8sgpt-ai/k8sgpt/commit/5a59abb55d9e76f3095b903ea973138b1afdccf2)) * add tests for ingress analyzer && Use t.Fatalf to report a fatal error if RunAnalysis returns an unexpected error ([e27e940](https://github.com/k8sgpt-ai/k8sgpt/commit/e27e9409dcaad78eefd79659e91364617407ae59)) ### Bug Fixes * **deps:** update module github.com/sashabaranov/go-openai to v1.6.1 ([#207](https://github.com/k8sgpt-ai/k8sgpt/issues/207)) ([eeac731](https://github.com/k8sgpt-ai/k8sgpt/commit/eeac731858999f6f462a7b6ccf210af603674b30)) * **deps:** update module github.com/spf13/cobra to v1.7.0 ([5d5e082](https://github.com/k8sgpt-ai/k8sgpt/commit/5d5e082f417905954be33b3a620efef674f2588d)) * **deps:** update module github.com/stretchr/testify to v1.8.2 ([f5e3ca0](https://github.com/k8sgpt-ai/k8sgpt/commit/f5e3ca0bcab9325145a2e1d8624f585ffee8e29f)) * **deps:** update module golang.org/x/term to v0.7.0 ([8ab3573](https://github.com/k8sgpt-ai/k8sgpt/commit/8ab3573e13a3e3ab98e6f0aa76e429117c888f7f)) * details in json output ([2f21002](https://github.com/k8sgpt-ai/k8sgpt/commit/2f2100289953af7820bbb01f2c980cf5492de079)) * fixed hpa tests after rebase ([a24d1f1](https://github.com/k8sgpt-ai/k8sgpt/commit/a24d1f1b304e9448e63c1b7fc283b4cc8bc639aa)) * regression on dynamic filters ([93bcc62](https://github.com/k8sgpt-ai/k8sgpt/commit/93bcc627ba64a9139e65290a8512e0a9b4bf1a69)) * Spelling ([ba4d701](https://github.com/k8sgpt-ai/k8sgpt/commit/ba4d7016814ce97353e98658d5bbcd692007e4a9)) ### Docs * add curl command and release-please annoations ([1849209](https://github.com/k8sgpt-ai/k8sgpt/commit/184920988f7da928cca7fae4a676e4ee5f13cad1)) * add guide to details block ([ddc120e](https://github.com/k8sgpt-ai/k8sgpt/commit/ddc120e7c2657385737d3490def28dbabdd2242d)) * add installation guide via packages ([8e4ce6a](https://github.com/k8sgpt-ai/k8sgpt/commit/8e4ce6a974813258fb9cbeabbcaa3b8f6966a748)) * minor change ([53c1330](https://github.com/k8sgpt-ai/k8sgpt/commit/53c13305383eb454fe45fefa3483cef4821d5d34)) * modify README ([fc47c58](https://github.com/k8sgpt-ai/k8sgpt/commit/fc47c58ae1c2b5511ebbe0ed35714e4ecbb4bb7a)) * modify README ([0f46ceb](https://github.com/k8sgpt-ai/k8sgpt/commit/0f46ceb4456a90e7e05aeff23d25d5775bbf9c2b)) ### Other * added initial tests for json output ([22e3166](https://github.com/k8sgpt-ai/k8sgpt/commit/22e31661bff27b28339898826a34ffdcfcff3583)) * analyzer and ai interfacing ([#200](https://github.com/k8sgpt-ai/k8sgpt/issues/200)) ([0195bfa](https://github.com/k8sgpt-ai/k8sgpt/commit/0195bfab30ab748b3bb7f1b8c8f0e988b99ee54d)) * **deps:** pin anchore/sbom-action action to 448520c ([#203](https://github.com/k8sgpt-ai/k8sgpt/issues/203)) ([9ff3fbc](https://github.com/k8sgpt-ai/k8sgpt/commit/9ff3fbc382ed78b55a7a1966ecdae186c03b2848)) * **deps:** update golang docker tag to v1.20.3 ([e9994b8](https://github.com/k8sgpt-ai/k8sgpt/commit/e9994b8d167d4f1d9c0d0dabf8385ff22cfd16a4)) * made json output prettier and improved output ([db40734](https://github.com/k8sgpt-ai/k8sgpt/commit/db40734a0db89850a2a685c9a7f5f5559875b7b3)) ## [0.1.8](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.1.7...v0.1.8) (2023-04-03) ### Features * add password flag for backend authentication ([#199](https://github.com/k8sgpt-ai/k8sgpt/issues/199)) ([075a940](https://github.com/k8sgpt-ai/k8sgpt/commit/075a940d2c9bdd8aa9162940ed46abad47d46998)) * adding shields to readme ([213ecd8](https://github.com/k8sgpt-ai/k8sgpt/commit/213ecd8e83933fabaa5d3d674c67958599dd72ce)) * adding unit testing and example ([35b838b](https://github.com/k8sgpt-ai/k8sgpt/commit/35b838bfafa248dbf3932c7a3ee708b1a1539f18)) * alias filter to filters ([dde4e83](https://github.com/k8sgpt-ai/k8sgpt/commit/dde4e833b0e87553dea4e5c1e17a14e303956bc1)) * analyzer ifacing ([426f562](https://github.com/k8sgpt-ai/k8sgpt/commit/426f562be83ed0e708a07b9e1900ac06fa017c27)) * service test ([44cc8f7](https://github.com/k8sgpt-ai/k8sgpt/commit/44cc8f7ad68d152ec577e57cab7d8d9ab9613378)) * test workflow ([5f30a4d](https://github.com/k8sgpt-ai/k8sgpt/commit/5f30a4ddf44ebff949bb0573f261667539a2dcfb)) ### Bug Fixes * **deps:** update module github.com/sashabaranov/go-openai to v1.5.8 ([91fb065](https://github.com/k8sgpt-ai/k8sgpt/commit/91fb06530a21259da6e72c28342e743d2b481294)) ### Other * create linux packages ([#201](https://github.com/k8sgpt-ai/k8sgpt/issues/201)) ([67753be](https://github.com/k8sgpt-ai/k8sgpt/commit/67753be6f317c462ebe1d9a316f2b0c9684ca4e5)) * **deps:** pin dependencies ([#198](https://github.com/k8sgpt-ai/k8sgpt/issues/198)) ([f8291aa](https://github.com/k8sgpt-ai/k8sgpt/commit/f8291aab085209f9fee13a6c92c96076163e2e90)) ## [0.1.7](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.1.6...v0.1.7) (2023-04-02) ### Features * add hpa analyzer and init additionalAnalyzers ([3603872](https://github.com/k8sgpt-ai/k8sgpt/commit/360387249feb9a999286aaa874a13007986219a5)) * add pda analyzer ([532a5ce](https://github.com/k8sgpt-ai/k8sgpt/commit/532a5ce0332a8466df42bc944800e6668e349801)) * check if ScaleTargetRef is possible option ([5dad75f](https://github.com/k8sgpt-ai/k8sgpt/commit/5dad75fbe9fd15cfa7bfa69c046b851ea905876f)) ### Bug Fixes * hpaAnalyzer analysis result is using wrong parent ([1190fe6](https://github.com/k8sgpt-ai/k8sgpt/commit/1190fe60fdd6e66ce435874628039df7047a52b9)) * spelling of PodDisruptionBudget ([ceff008](https://github.com/k8sgpt-ai/k8sgpt/commit/ceff0084df1b6de16f1ed503ee8a4b3c1a9f8648)) * update client API call to use StatefulSet instead of Deployment ([4916fef](https://github.com/k8sgpt-ai/k8sgpt/commit/4916fef9d6b75c54bcfbc5d136550018e96e3632)) ### Refactoring * merged main into branch ([3e836d8](https://github.com/k8sgpt-ai/k8sgpt/commit/3e836d81b7c33ce5c0c133c2e1ca3b0c8d3eeeb0)), closes [#101](https://github.com/k8sgpt-ai/k8sgpt/issues/101) ### Other * **deps:** update anchore/sbom-action action to v0.14.1 ([80f29da](https://github.com/k8sgpt-ai/k8sgpt/commit/80f29dae4fd6f6348967192ce2f51f0e0fb5dea0)) * merge branch 'chetanguptaa-some-fixes' ([071ee56](https://github.com/k8sgpt-ai/k8sgpt/commit/071ee560f36b64b4c65274181e2d13bb14d5b914)) * refine renovate config ([#172](https://github.com/k8sgpt-ai/k8sgpt/issues/172)) ([d23da9a](https://github.com/k8sgpt-ai/k8sgpt/commit/d23da9ae836a07f0fd59c20a1c3c71d6b7f75277)) * removes bar on normal analyze events ([e1d8992](https://github.com/k8sgpt-ai/k8sgpt/commit/e1d89920b097db4417c55b020fb23dd8cbaf19ed)) * removes bar on normal analyze events ([96d0d75](https://github.com/k8sgpt-ai/k8sgpt/commit/96d0d754eab67c0742d3a36a1eefb9c28df59e96)) * update dependencies ([#174](https://github.com/k8sgpt-ai/k8sgpt/issues/174)) ([9d9c262](https://github.com/k8sgpt-ai/k8sgpt/commit/9d9c26214fbb4c4faba7ef85f2204bc961396de8)) ### Docs * add pdbAnalyzer as optional analyzer ([f6974d0](https://github.com/k8sgpt-ai/k8sgpt/commit/f6974d07581384e260059f121242854320dfc58b)) ## [0.1.6](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.1.5...v0.1.6) (2023-03-31) ### Bug Fixes * analysis detail not displayed when --explain ([869ba90](https://github.com/k8sgpt-ai/k8sgpt/commit/869ba909075a5543413fb6ae7fc79aa067c08da4)) ## [0.1.5](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.1.4...v0.1.5) (2023-03-31) ### Features * add & remove default filter(s) to analyze. ([32ddf66](https://github.com/k8sgpt-ai/k8sgpt/commit/32ddf6691ce083fd4283a1d5ac4b9f02e90df867)) * add filter command add "list" subcommand ([#159](https://github.com/k8sgpt-ai/k8sgpt/issues/159)) ([6e17c9e](https://github.com/k8sgpt-ai/k8sgpt/commit/6e17c9e285e3871bb8f694b734a8cd6fd02e60f0)) * check if filters does not empty on add & remove ([975813d](https://github.com/k8sgpt-ai/k8sgpt/commit/975813d3284719c877630ad20f90c6fe163283da)) * remove filter prefix on subcommand ([30faf84](https://github.com/k8sgpt-ai/k8sgpt/commit/30faf842541c0be6b6483f71f6cf04d5cafecef5)) * rework filters ([3ed545f](https://github.com/k8sgpt-ai/k8sgpt/commit/3ed545f33fb3ecb3827c03e8c89027c61386c44f)) * update filters add & remove to be more consistent ([9aa0e89](https://github.com/k8sgpt-ai/k8sgpt/commit/9aa0e8960ee340208b4749954c99867842ba58b9)) ### Bug Fixes * kubecontext flag has no effect ([a8bf451](https://github.com/k8sgpt-ai/k8sgpt/commit/a8bf45134ff3a72dc3e531d720f119790faff9d4)) * spelling on dupplicateFilters ([0a12448](https://github.com/k8sgpt-ai/k8sgpt/commit/0a124484a23789376258413e73628c7b1d7abded)) ### Other * renamed filter list file ([25f8dc3](https://github.com/k8sgpt-ai/k8sgpt/commit/25f8dc390cccd66965993f464351e671af11f8ac)) ## [0.1.4](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.1.3...v0.1.4) (2023-03-30) ### Features * add Ingress class validation ([#154](https://github.com/k8sgpt-ai/k8sgpt/issues/154)) ([b061566](https://github.com/k8sgpt-ai/k8sgpt/commit/b061566404ef80288ca29add2d401574109d44c0)) * output selected backend ([#153](https://github.com/k8sgpt-ai/k8sgpt/issues/153)) ([be061da](https://github.com/k8sgpt-ai/k8sgpt/commit/be061da5b65045938acd70ad2eb2d21b87d2d6bf)) ### Bug Fixes * now supports different kubeconfig and kubectx ([c8f3c94](https://github.com/k8sgpt-ai/k8sgpt/commit/c8f3c946b00c00cd185961a4fa777806da94014e)) ### Refactoring * removed sample flag ([0afd528](https://github.com/k8sgpt-ai/k8sgpt/commit/0afd52844b96579391f77698bf0555145b6d2be8)) ## [0.1.3](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.1.2...v0.1.3) (2023-03-30) ### Features * add secret validation to ingress analyzer ([#141](https://github.com/k8sgpt-ai/k8sgpt/issues/141)) ([86c7e81](https://github.com/k8sgpt-ai/k8sgpt/commit/86c7e81e18db02ebcbfe35d470682c982871375f)) * bugfix for output ([2eab0c5](https://github.com/k8sgpt-ai/k8sgpt/commit/2eab0c544fbb6026f6aea79b08d8f29c061acf2e)) * CODE_OF_CONDUCT.md ([#129](https://github.com/k8sgpt-ai/k8sgpt/issues/129)) ([fe73633](https://github.com/k8sgpt-ai/k8sgpt/commit/fe73633273c5c1f4188bca48471283535967d5aa)) * create-security.md ([27b8916](https://github.com/k8sgpt-ai/k8sgpt/commit/27b8916f297570907437686c6d958636fb249d50)) * improvement to analysis speed ([548039e](https://github.com/k8sgpt-ai/k8sgpt/commit/548039ebe62bb609c1aa288e5e49845850fd2dd8)) * init ingress analyzer ([#138](https://github.com/k8sgpt-ai/k8sgpt/issues/138)) ([fe683b7](https://github.com/k8sgpt-ai/k8sgpt/commit/fe683b71b84fe82459b0ffe366b4dcfa1c978cfe)) ### Bug Fixes * add Ingress in GetParent switch case ([14ba8d5](https://github.com/k8sgpt-ai/k8sgpt/commit/14ba8d555005f31fc2201cb8b61653093c19b8a7)) * bugfix for output ([#148](https://github.com/k8sgpt-ai/k8sgpt/issues/148)) ([172c2df](https://github.com/k8sgpt-ai/k8sgpt/commit/172c2df6c55f5fddbfec7f8526be5f2323d1b900)) * Change ObjectMeta value in Ingress analyser. ([bf49a51](https://github.com/k8sgpt-ai/k8sgpt/commit/bf49a51c62af450cff51a590547ef30989bd2e93)) * typo in description of the filter flag in analyze command ([#147](https://github.com/k8sgpt-ai/k8sgpt/issues/147)) ([f4765be](https://github.com/k8sgpt-ai/k8sgpt/commit/f4765bed1b1ad121a81b35878fdb866354b5e34a)) ### Other * **deps:** update google-github-actions/release-please-action digest to ee9822e ([#132](https://github.com/k8sgpt-ai/k8sgpt/issues/132)) ([01b2826](https://github.com/k8sgpt-ai/k8sgpt/commit/01b282647512a4eaebd42ab5847b5534de148d14)) ### Docs * add new slack link ([#134](https://github.com/k8sgpt-ai/k8sgpt/issues/134)) ([#135](https://github.com/k8sgpt-ai/k8sgpt/issues/135)) ([cad2b38](https://github.com/k8sgpt-ai/k8sgpt/commit/cad2b38d037658495024ec0166ebd3e936f65c2e)) ## [0.1.2](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.1.1...v0.1.2) (2023-03-28) ### Features * added namespace filter ([#127](https://github.com/k8sgpt-ai/k8sgpt/issues/127)) ([b78ab3d](https://github.com/k8sgpt-ai/k8sgpt/commit/b78ab3d9b503a256bf6ccf18276e20140ae17d1c)) * prefix templates ([#125](https://github.com/k8sgpt-ai/k8sgpt/issues/125)) ([65a568e](https://github.com/k8sgpt-ai/k8sgpt/commit/65a568e937a8fdacc179f5e8b1a021a0178c04f0)) ### Bug Fixes * readme code blocks ([#126](https://github.com/k8sgpt-ai/k8sgpt/issues/126)) ([c8b92aa](https://github.com/k8sgpt-ai/k8sgpt/commit/c8b92aaa0e2795aa8d65f84277c8adfe0f1d14e3)) * update README.md ([#119](https://github.com/k8sgpt-ai/k8sgpt/issues/119)) ([05abe97](https://github.com/k8sgpt-ai/k8sgpt/commit/05abe975dd859cd85096a1a7182f17b0437ad20f)) ### Other * added default issue template ([#96](https://github.com/k8sgpt-ai/k8sgpt/issues/96)) ([#121](https://github.com/k8sgpt-ai/k8sgpt/issues/121)) ([11c227b](https://github.com/k8sgpt-ai/k8sgpt/commit/11c227b82e16dac8b46cbd03bb04d9cc1c2b5ac3)) ### Docs * add new issue templates ([dbd305f](https://github.com/k8sgpt-ai/k8sgpt/commit/dbd305f901cca09b7148254c3aa7a7435504d6cc)) * add WSL gcc instructions ([4d5566b](https://github.com/k8sgpt-ai/k8sgpt/commit/4d5566b4df7aedf43edbeeb03130f0ba77dbed1a)) * added Windows and Linux instalation steps in README ([#116](https://github.com/k8sgpt-ai/k8sgpt/issues/116)) ([3bfb278](https://github.com/k8sgpt-ai/k8sgpt/commit/3bfb278f81a9c550ee37a88c0cb0377331802542)) * fix indentations ([a46416d](https://github.com/k8sgpt-ai/k8sgpt/commit/a46416dce0f5cee2d42b27525023b04af1a8e3c0)) * rename ISSUE_TEMPLATE ([#124](https://github.com/k8sgpt-ai/k8sgpt/issues/124)) ([cb4932c](https://github.com/k8sgpt-ai/k8sgpt/commit/cb4932c39df4903a4b48ae5f0428860027f76fd2)) ## [0.1.1](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.1.0...v0.1.1) (2023-03-28) ### Features * this stops service exiting the program ([6f90386](https://github.com/k8sgpt-ai/k8sgpt/commit/6f90386fc93b2e39e59832468922e8ba7210b8e5)) * updated readme ([e0141d1](https://github.com/k8sgpt-ai/k8sgpt/commit/e0141d1cf54b5b37b25a5caeb9d5c940b9410ea7)) ### Bug Fixes * short term solution for exhaustion ([5890e3a](https://github.com/k8sgpt-ai/k8sgpt/commit/5890e3a79c80a2973af2feb7d50e7f9c57c563c2)) ### Other * update README.md ([93b947f](https://github.com/k8sgpt-ai/k8sgpt/commit/93b947f261e401c10dde6dc1854e6e22187437d6)) * update root.go path ([2cb1c9c](https://github.com/k8sgpt-ai/k8sgpt/commit/2cb1c9c150d052bb3942d9f62ded9d54b0e1873e)) ## [0.1.0](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.0.9...v0.1.0) (2023-03-28) ### Features * added british alias ([39c0444](https://github.com/k8sgpt-ai/k8sgpt/commit/39c0444fac9b46d0faa347b45df779b97019e5b6)), closes [#93](https://github.com/k8sgpt-ai/k8sgpt/issues/93) * enables overwriting of cache ([#95](https://github.com/k8sgpt-ai/k8sgpt/issues/95)) ([a270f7c](https://github.com/k8sgpt-ai/k8sgpt/commit/a270f7c89fb8bec35984715c5e4d160a2307e678)) ### Other * add CODEOWNERS ([c5c6162](https://github.com/k8sgpt-ai/k8sgpt/commit/c5c6162df1f3701659e47bce6e9fc6e3c569e539)) * add codeowners file ([#102](https://github.com/k8sgpt-ai/k8sgpt/issues/102)) ([829ff56](https://github.com/k8sgpt-ai/k8sgpt/commit/829ff566c0a964250d3d8d45306d410e1b9d9d35)) * release 0.1.0 ([f9c7daf](https://github.com/k8sgpt-ai/k8sgpt/commit/f9c7daf3dcd06dcd9cea5603108b8a42ee273348)) ## [0.0.9](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.0.8...v0.0.9) (2023-03-28) ### Other * small update ([202e8e2](https://github.com/k8sgpt-ai/k8sgpt/commit/202e8e2977422b2b4506a80dc9b76a392c5457eb)) ## [0.0.8](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.0.7...v0.0.8) (2023-03-27) ### Features * add generation of api-keys to cli ([#87](https://github.com/k8sgpt-ai/k8sgpt/issues/87)) ([1c653ec](https://github.com/k8sgpt-ai/k8sgpt/commit/1c653ecc51b74a2f51ce7240ffaee0fe75f2e8dd)) * add generation of api-keys to cli ([#87](https://github.com/k8sgpt-ai/k8sgpt/issues/87)) ([bb2db5c](https://github.com/k8sgpt-ai/k8sgpt/commit/bb2db5ca7923e2049308d1674bb59ae8154e415c)) * addition of simple language support ([c3008c5](https://github.com/k8sgpt-ai/k8sgpt/commit/c3008c5e75acbb35d864135199ca9c034f59e35f)) * version ([0c231d6](https://github.com/k8sgpt-ai/k8sgpt/commit/0c231d635e7ad71609bb80abac5e0ade15ffb860)) * version ([931f072](https://github.com/k8sgpt-ai/k8sgpt/commit/931f072e0ab0cfd77f261b0b719cf0819f85b951)) ## [0.0.7](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.0.6...v0.0.7) (2023-03-27) ### Features * wip fixing missing details ([0852c65](https://github.com/k8sgpt-ai/k8sgpt/commit/0852c658ded33b91e1d323bd8cba6ac6935cb525)) ### Other * moved code ([a194d4a](https://github.com/k8sgpt-ai/k8sgpt/commit/a194d4a509329cbc5a00724b0a19c75726c2a0d3)) * return success on no issues ([009f47c](https://github.com/k8sgpt-ai/k8sgpt/commit/009f47c8e8ee6d3ce9b36110c36edae97690c949)) * updated readme ([06fb807](https://github.com/k8sgpt-ai/k8sgpt/commit/06fb8073dc5b0b5bd9f8d115d9ec206ab238d68f)) ## [0.0.6](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.0.6...v0.0.6) (2023-03-26) ### Features * add service analysis ([961fb6c](https://github.com/k8sgpt-ai/k8sgpt/commit/961fb6c555f59f1276531f462739b76b1508830e)) * added analysis for pvcs ([88d49ae](https://github.com/k8sgpt-ai/k8sgpt/commit/88d49ae21c7d889d59361de157360f80503683be)) * also fixes bug if the events feed is empty ([#73](https://github.com/k8sgpt-ai/k8sgpt/issues/73)) ([a1093dc](https://github.com/k8sgpt-ai/k8sgpt/commit/a1093dcfe468a7671c9e543372f73780fb38418e)) * build container ([260640f](https://github.com/k8sgpt-ai/k8sgpt/commit/260640f865baefba8ac256f800d4992f25ca15fd)) * find parent objects ([b29c6e4](https://github.com/k8sgpt-ai/k8sgpt/commit/b29c6e45825807d07dd6fdb954457772f40b1b0e)) * find parent objects and add information about them ([#72](https://github.com/k8sgpt-ai/k8sgpt/issues/72)) ([14e85b0](https://github.com/k8sgpt-ai/k8sgpt/commit/14e85b08ff7d9a571796905260db7f1056b6e838)) * find replicaset errors ([8ac56e0](https://github.com/k8sgpt-ai/k8sgpt/commit/8ac56e062baef2a0cf7c7ce2b4c97753f079f157)) * initial json implementation ([#68](https://github.com/k8sgpt-ai/k8sgpt/issues/68)) ([979f13f](https://github.com/k8sgpt-ai/k8sgpt/commit/979f13f043f54a5bc74d0a49fee0db2faaf0a4f8)) * interfaced out ai clients ([90b3c08](https://github.com/k8sgpt-ai/k8sgpt/commit/90b3c0898c8ab1299ce8b60effe981f5fc9ed63b)) * support for multi-auth ([51aa59a](https://github.com/k8sgpt-ai/k8sgpt/commit/51aa59aea8c0fd5533d2300c7a79c0b9008ef887)) * updated readme ([7336924](https://github.com/k8sgpt-ai/k8sgpt/commit/73369240b4fc8c91dae0ae272e671f7b413e3bdc)) ### Bug Fixes * add permissions to read repository ([d6cc4cf](https://github.com/k8sgpt-ai/k8sgpt/commit/d6cc4cfcbffbf84f27c7e4e4159da1e42dd5d689)) * build ([1fbed3e](https://github.com/k8sgpt-ai/k8sgpt/commit/1fbed3e44ff790fccfef502ddafae92e34629c21)) * container naming ([115276e](https://github.com/k8sgpt-ai/k8sgpt/commit/115276e01a38fc1692d6b66ab56a33f1e1793974)) * **deps:** update module github.com/sashabaranov/go-openai to v1.5.5 ([105fe44](https://github.com/k8sgpt-ai/k8sgpt/commit/105fe44680e5a987d4a65ff9c58b5b2211808c5e)) * **deps:** update module github.com/sashabaranov/go-openai to v1.5.6 ([37a1d3f](https://github.com/k8sgpt-ai/k8sgpt/commit/37a1d3f47e07caddb168f228627973870a9d867e)) * **deps:** update module github.com/sashabaranov/go-openai to v1.5.7 ([7f7726d](https://github.com/k8sgpt-ai/k8sgpt/commit/7f7726d59a63baeaf8ff110e00b30a20ec7f1df5)) * minor adaptions ([ef17b84](https://github.com/k8sgpt-ai/k8sgpt/commit/ef17b845ba3c65c16ed5dcc417e3e3d3d40dd04e)) * missing parent when explain is used ([9c7d559](https://github.com/k8sgpt-ai/k8sgpt/commit/9c7d55955b777ad201307cb946e0fc81cf9c4b99)) * release please config ([c402c7b](https://github.com/k8sgpt-ai/k8sgpt/commit/c402c7bab7baababbbc7c82965d8337de7d50d35)) * remove sboms from goreleaser ([addc01f](https://github.com/k8sgpt-ai/k8sgpt/commit/addc01f700dd2ea31ec24dcf4995bb7ed4a4785e)) * semantic commit token permission ([#69](https://github.com/k8sgpt-ai/k8sgpt/issues/69)) ([0181c0a](https://github.com/k8sgpt-ai/k8sgpt/commit/0181c0aeb56ad82fd232ce1c7788c43b7bd03bf2)) ### Docs * add some important information to contributing ([9ab7f58](https://github.com/k8sgpt-ai/k8sgpt/commit/9ab7f587620d69e4e8fc98faabce6417c35f7497)) * update CONTRIBUTING ([05a787d](https://github.com/k8sgpt-ai/k8sgpt/commit/05a787d53dfe5e625c6449ac1e21ec36e66ddd28)) * update CONTRIBUTING ([26449e1](https://github.com/k8sgpt-ai/k8sgpt/commit/26449e10efd8926cccd4a2eaa4e9dc3afa8bd01a)) ### Other * add bot secret to goreleaser ([171e58b](https://github.com/k8sgpt-ai/k8sgpt/commit/171e58b51107f75717694e35c4e249ee41f0409a)) * add brew tap generation on release ([2992c4e](https://github.com/k8sgpt-ai/k8sgpt/commit/2992c4e5c8abad50c90ed85523c732f19ab1f31c)) * add initial renovate config ([e37dbc7](https://github.com/k8sgpt-ai/k8sgpt/commit/e37dbc7909f1c520c4c6660c25b45de5847ea581)) * add pull request template ([a6d5132](https://github.com/k8sgpt-ai/k8sgpt/commit/a6d5132b8c2ff077680e2edfd8361a93008197fd)) * add release-please ([da7b409](https://github.com/k8sgpt-ai/k8sgpt/commit/da7b40978d55a6afed4c3a1ca83a756238feaca8)) * add semantic pr validation ([#66](https://github.com/k8sgpt-ai/k8sgpt/issues/66)) ([ad594c7](https://github.com/k8sgpt-ai/k8sgpt/commit/ad594c7cb2105e0eff72d1767b2ddcc4dc0e3d38)) * change module repo ([a307c13](https://github.com/k8sgpt-ai/k8sgpt/commit/a307c132b3464ff2e949c8a5588e01d344de91a0)) * **deps:** pin amannn/action-semantic-pull-request action to c3cd5d1 ([3621766](https://github.com/k8sgpt-ai/k8sgpt/commit/36217667ceb87d9b97b44dc91e0ff6e7a1b86e14)) * **deps:** pin dependencies ([f6072f5](https://github.com/k8sgpt-ai/k8sgpt/commit/f6072f56cbe2c073b7b7ebef6c12fa98120e54e2)) * **deps:** pin dependencies ([5b360de](https://github.com/k8sgpt-ai/k8sgpt/commit/5b360de2ae6094cf850a4ae973a22855c21a9040)) * **deps:** pin dependencies ([7fea7d1](https://github.com/k8sgpt-ai/k8sgpt/commit/7fea7d14a572fe0fd05f5f241b98e93655fb1965)) * **deps:** update actions/checkout digest to 8f4b7f8 ([9955d75](https://github.com/k8sgpt-ai/k8sgpt/commit/9955d754505b60f28d17397132a1d02e95ffe303)) * **main:** release 0.0.3 ([53c9947](https://github.com/k8sgpt-ai/k8sgpt/commit/53c994725ea2c2c54898ffe5307d9df40e9c1fe5)) * **main:** release 0.0.3 ([f5d8609](https://github.com/k8sgpt-ai/k8sgpt/commit/f5d86092f49faef8d71cb950986d76c3f92daf46)) * **main:** release 0.0.3 ([22873a6](https://github.com/k8sgpt-ai/k8sgpt/commit/22873a67163e58484d2a0ad343b4ba3c83e51d8f)) * **main:** release 0.0.4 ([13b7d58](https://github.com/k8sgpt-ai/k8sgpt/commit/13b7d58e590078f086a0af2f9d1800e0e65a28bb)) * **main:** release 0.0.4 ([aef7256](https://github.com/k8sgpt-ai/k8sgpt/commit/aef7256dc3a85817573744f8b4a54f834368bac7)) * **main:** release 0.0.4 ([6dbcde9](https://github.com/k8sgpt-ai/k8sgpt/commit/6dbcde94e961a6e5a1fc0559d2a1da5567a659de)) * **main:** release 0.0.5 ([9fecc1e](https://github.com/k8sgpt-ai/k8sgpt/commit/9fecc1ea6df4104412fc1230372de6f26aa1ade2)) * **main:** release 0.0.6 ([d554bba](https://github.com/k8sgpt-ai/k8sgpt/commit/d554bba38494745f83b5a8931f665429af35a31a)) * release 0.0.3 ([4840aa0](https://github.com/k8sgpt-ai/k8sgpt/commit/4840aa081e3aa4a7a01fd3fd5f837fa6f0c3c02c)) * release 0.0.3 ([de02795](https://github.com/k8sgpt-ai/k8sgpt/commit/de027955ea18a751c5f991e7ff0f60b90ae704b0)) * release 0.0.3 ([a927c32](https://github.com/k8sgpt-ai/k8sgpt/commit/a927c32def806bb8b99e1cfcd4ee3dcdeca6ae5d)) * release 0.0.4 ([08f2c31](https://github.com/k8sgpt-ai/k8sgpt/commit/08f2c3112e2cc16b49b9cf8fdbd97368acecc754)) * release 0.0.5 ([8da8945](https://github.com/k8sgpt-ai/k8sgpt/commit/8da8945d1b8d898440be235f88bdb2c08b0f9f84)) * release 0.0.6 ([dc2bfa9](https://github.com/k8sgpt-ai/k8sgpt/commit/dc2bfa918c080a6c1b2e5ef66d699d9e08e28e10)) ## [0.0.6](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.0.5...v0.0.6) (2023-03-26) ### Features * add service analysis ([961fb6c](https://github.com/k8sgpt-ai/k8sgpt/commit/961fb6c555f59f1276531f462739b76b1508830e)) * added analysis for pvcs ([88d49ae](https://github.com/k8sgpt-ai/k8sgpt/commit/88d49ae21c7d889d59361de157360f80503683be)) * also fixes bug if the events feed is empty ([#73](https://github.com/k8sgpt-ai/k8sgpt/issues/73)) ([a1093dc](https://github.com/k8sgpt-ai/k8sgpt/commit/a1093dcfe468a7671c9e543372f73780fb38418e)) * find parent objects ([b29c6e4](https://github.com/k8sgpt-ai/k8sgpt/commit/b29c6e45825807d07dd6fdb954457772f40b1b0e)) * find parent objects and add information about them ([#72](https://github.com/k8sgpt-ai/k8sgpt/issues/72)) ([14e85b0](https://github.com/k8sgpt-ai/k8sgpt/commit/14e85b08ff7d9a571796905260db7f1056b6e838)) * initial json implementation ([#68](https://github.com/k8sgpt-ai/k8sgpt/issues/68)) ([979f13f](https://github.com/k8sgpt-ai/k8sgpt/commit/979f13f043f54a5bc74d0a49fee0db2faaf0a4f8)) * interfaced out ai clients ([90b3c08](https://github.com/k8sgpt-ai/k8sgpt/commit/90b3c0898c8ab1299ce8b60effe981f5fc9ed63b)) * support for multi-auth ([51aa59a](https://github.com/k8sgpt-ai/k8sgpt/commit/51aa59aea8c0fd5533d2300c7a79c0b9008ef887)) ### Bug Fixes * missing parent when explain is used ([9c7d559](https://github.com/k8sgpt-ai/k8sgpt/commit/9c7d55955b777ad201307cb946e0fc81cf9c4b99)) * semantic commit token permission ([#69](https://github.com/k8sgpt-ai/k8sgpt/issues/69)) ([0181c0a](https://github.com/k8sgpt-ai/k8sgpt/commit/0181c0aeb56ad82fd232ce1c7788c43b7bd03bf2)) ### Other * add semantic pr validation ([#66](https://github.com/k8sgpt-ai/k8sgpt/issues/66)) ([ad594c7](https://github.com/k8sgpt-ai/k8sgpt/commit/ad594c7cb2105e0eff72d1767b2ddcc4dc0e3d38)) * **deps:** pin amannn/action-semantic-pull-request action to c3cd5d1 ([3621766](https://github.com/k8sgpt-ai/k8sgpt/commit/36217667ceb87d9b97b44dc91e0ff6e7a1b86e14)) ## [0.0.5](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.0.4...v0.0.5) (2023-03-24) ### Other * release 0.0.5 ([8da8945](https://github.com/k8sgpt-ai/k8sgpt/commit/8da8945d1b8d898440be235f88bdb2c08b0f9f84)) ## [0.0.4](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.0.4...v0.0.4) (2023-03-24) ### Features * build container ([260640f](https://github.com/k8sgpt-ai/k8sgpt/commit/260640f865baefba8ac256f800d4992f25ca15fd)) * find replicaset errors ([8ac56e0](https://github.com/k8sgpt-ai/k8sgpt/commit/8ac56e062baef2a0cf7c7ce2b4c97753f079f157)) ### Bug Fixes * add permissions to read repository ([d6cc4cf](https://github.com/k8sgpt-ai/k8sgpt/commit/d6cc4cfcbffbf84f27c7e4e4159da1e42dd5d689)) * build ([1fbed3e](https://github.com/k8sgpt-ai/k8sgpt/commit/1fbed3e44ff790fccfef502ddafae92e34629c21)) * container naming ([115276e](https://github.com/k8sgpt-ai/k8sgpt/commit/115276e01a38fc1692d6b66ab56a33f1e1793974)) * **deps:** update module github.com/sashabaranov/go-openai to v1.5.5 ([105fe44](https://github.com/k8sgpt-ai/k8sgpt/commit/105fe44680e5a987d4a65ff9c58b5b2211808c5e)) * **deps:** update module github.com/sashabaranov/go-openai to v1.5.6 ([37a1d3f](https://github.com/k8sgpt-ai/k8sgpt/commit/37a1d3f47e07caddb168f228627973870a9d867e)) * **deps:** update module github.com/sashabaranov/go-openai to v1.5.7 ([7f7726d](https://github.com/k8sgpt-ai/k8sgpt/commit/7f7726d59a63baeaf8ff110e00b30a20ec7f1df5)) * minor adaptions ([ef17b84](https://github.com/k8sgpt-ai/k8sgpt/commit/ef17b845ba3c65c16ed5dcc417e3e3d3d40dd04e)) * release please config ([c402c7b](https://github.com/k8sgpt-ai/k8sgpt/commit/c402c7bab7baababbbc7c82965d8337de7d50d35)) * remove sboms from goreleaser ([addc01f](https://github.com/k8sgpt-ai/k8sgpt/commit/addc01f700dd2ea31ec24dcf4995bb7ed4a4785e)) ### Docs * add some important information to contributing ([9ab7f58](https://github.com/k8sgpt-ai/k8sgpt/commit/9ab7f587620d69e4e8fc98faabce6417c35f7497)) * update CONTRIBUTING ([05a787d](https://github.com/k8sgpt-ai/k8sgpt/commit/05a787d53dfe5e625c6449ac1e21ec36e66ddd28)) * update CONTRIBUTING ([26449e1](https://github.com/k8sgpt-ai/k8sgpt/commit/26449e10efd8926cccd4a2eaa4e9dc3afa8bd01a)) ### Other * add bot secret to goreleaser ([171e58b](https://github.com/k8sgpt-ai/k8sgpt/commit/171e58b51107f75717694e35c4e249ee41f0409a)) * add brew tap generation on release ([2992c4e](https://github.com/k8sgpt-ai/k8sgpt/commit/2992c4e5c8abad50c90ed85523c732f19ab1f31c)) * add initial renovate config ([e37dbc7](https://github.com/k8sgpt-ai/k8sgpt/commit/e37dbc7909f1c520c4c6660c25b45de5847ea581)) * add pull request template ([a6d5132](https://github.com/k8sgpt-ai/k8sgpt/commit/a6d5132b8c2ff077680e2edfd8361a93008197fd)) * add release-please ([da7b409](https://github.com/k8sgpt-ai/k8sgpt/commit/da7b40978d55a6afed4c3a1ca83a756238feaca8)) * change module repo ([a307c13](https://github.com/k8sgpt-ai/k8sgpt/commit/a307c132b3464ff2e949c8a5588e01d344de91a0)) * **deps:** pin dependencies ([f6072f5](https://github.com/k8sgpt-ai/k8sgpt/commit/f6072f56cbe2c073b7b7ebef6c12fa98120e54e2)) * **deps:** pin dependencies ([5b360de](https://github.com/k8sgpt-ai/k8sgpt/commit/5b360de2ae6094cf850a4ae973a22855c21a9040)) * **deps:** pin dependencies ([7fea7d1](https://github.com/k8sgpt-ai/k8sgpt/commit/7fea7d14a572fe0fd05f5f241b98e93655fb1965)) * **deps:** update actions/checkout digest to 8f4b7f8 ([9955d75](https://github.com/k8sgpt-ai/k8sgpt/commit/9955d754505b60f28d17397132a1d02e95ffe303)) * **main:** release 0.0.3 ([53c9947](https://github.com/k8sgpt-ai/k8sgpt/commit/53c994725ea2c2c54898ffe5307d9df40e9c1fe5)) * **main:** release 0.0.3 ([f5d8609](https://github.com/k8sgpt-ai/k8sgpt/commit/f5d86092f49faef8d71cb950986d76c3f92daf46)) * **main:** release 0.0.3 ([22873a6](https://github.com/k8sgpt-ai/k8sgpt/commit/22873a67163e58484d2a0ad343b4ba3c83e51d8f)) * **main:** release 0.0.4 ([aef7256](https://github.com/k8sgpt-ai/k8sgpt/commit/aef7256dc3a85817573744f8b4a54f834368bac7)) * **main:** release 0.0.4 ([6dbcde9](https://github.com/k8sgpt-ai/k8sgpt/commit/6dbcde94e961a6e5a1fc0559d2a1da5567a659de)) * release 0.0.3 ([4840aa0](https://github.com/k8sgpt-ai/k8sgpt/commit/4840aa081e3aa4a7a01fd3fd5f837fa6f0c3c02c)) * release 0.0.3 ([de02795](https://github.com/k8sgpt-ai/k8sgpt/commit/de027955ea18a751c5f991e7ff0f60b90ae704b0)) * release 0.0.3 ([a927c32](https://github.com/k8sgpt-ai/k8sgpt/commit/a927c32def806bb8b99e1cfcd4ee3dcdeca6ae5d)) * release 0.0.4 ([08f2c31](https://github.com/k8sgpt-ai/k8sgpt/commit/08f2c3112e2cc16b49b9cf8fdbd97368acecc754)) ## [0.0.4](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.0.4...v0.0.4) (2023-03-24) ### Features * build container ([260640f](https://github.com/k8sgpt-ai/k8sgpt/commit/260640f865baefba8ac256f800d4992f25ca15fd)) * find replicaset errors ([8ac56e0](https://github.com/k8sgpt-ai/k8sgpt/commit/8ac56e062baef2a0cf7c7ce2b4c97753f079f157)) ### Bug Fixes * add permissions to read repository ([d6cc4cf](https://github.com/k8sgpt-ai/k8sgpt/commit/d6cc4cfcbffbf84f27c7e4e4159da1e42dd5d689)) * build ([1fbed3e](https://github.com/k8sgpt-ai/k8sgpt/commit/1fbed3e44ff790fccfef502ddafae92e34629c21)) * container naming ([115276e](https://github.com/k8sgpt-ai/k8sgpt/commit/115276e01a38fc1692d6b66ab56a33f1e1793974)) * **deps:** update module github.com/sashabaranov/go-openai to v1.5.5 ([105fe44](https://github.com/k8sgpt-ai/k8sgpt/commit/105fe44680e5a987d4a65ff9c58b5b2211808c5e)) * **deps:** update module github.com/sashabaranov/go-openai to v1.5.6 ([37a1d3f](https://github.com/k8sgpt-ai/k8sgpt/commit/37a1d3f47e07caddb168f228627973870a9d867e)) * **deps:** update module github.com/sashabaranov/go-openai to v1.5.7 ([7f7726d](https://github.com/k8sgpt-ai/k8sgpt/commit/7f7726d59a63baeaf8ff110e00b30a20ec7f1df5)) * minor adaptions ([ef17b84](https://github.com/k8sgpt-ai/k8sgpt/commit/ef17b845ba3c65c16ed5dcc417e3e3d3d40dd04e)) * release please config ([c402c7b](https://github.com/k8sgpt-ai/k8sgpt/commit/c402c7bab7baababbbc7c82965d8337de7d50d35)) * remove sboms from goreleaser ([addc01f](https://github.com/k8sgpt-ai/k8sgpt/commit/addc01f700dd2ea31ec24dcf4995bb7ed4a4785e)) ### Docs * add some important information to contributing ([9ab7f58](https://github.com/k8sgpt-ai/k8sgpt/commit/9ab7f587620d69e4e8fc98faabce6417c35f7497)) * update CONTRIBUTING ([05a787d](https://github.com/k8sgpt-ai/k8sgpt/commit/05a787d53dfe5e625c6449ac1e21ec36e66ddd28)) * update CONTRIBUTING ([26449e1](https://github.com/k8sgpt-ai/k8sgpt/commit/26449e10efd8926cccd4a2eaa4e9dc3afa8bd01a)) ### Other * add bot secret to goreleaser ([171e58b](https://github.com/k8sgpt-ai/k8sgpt/commit/171e58b51107f75717694e35c4e249ee41f0409a)) * add brew tap generation on release ([2992c4e](https://github.com/k8sgpt-ai/k8sgpt/commit/2992c4e5c8abad50c90ed85523c732f19ab1f31c)) * add initial renovate config ([e37dbc7](https://github.com/k8sgpt-ai/k8sgpt/commit/e37dbc7909f1c520c4c6660c25b45de5847ea581)) * add pull request template ([a6d5132](https://github.com/k8sgpt-ai/k8sgpt/commit/a6d5132b8c2ff077680e2edfd8361a93008197fd)) * add release-please ([da7b409](https://github.com/k8sgpt-ai/k8sgpt/commit/da7b40978d55a6afed4c3a1ca83a756238feaca8)) * change module repo ([a307c13](https://github.com/k8sgpt-ai/k8sgpt/commit/a307c132b3464ff2e949c8a5588e01d344de91a0)) * **deps:** pin dependencies ([f6072f5](https://github.com/k8sgpt-ai/k8sgpt/commit/f6072f56cbe2c073b7b7ebef6c12fa98120e54e2)) * **deps:** pin dependencies ([5b360de](https://github.com/k8sgpt-ai/k8sgpt/commit/5b360de2ae6094cf850a4ae973a22855c21a9040)) * **deps:** pin dependencies ([7fea7d1](https://github.com/k8sgpt-ai/k8sgpt/commit/7fea7d14a572fe0fd05f5f241b98e93655fb1965)) * **deps:** update actions/checkout digest to 8f4b7f8 ([9955d75](https://github.com/k8sgpt-ai/k8sgpt/commit/9955d754505b60f28d17397132a1d02e95ffe303)) * **main:** release 0.0.3 ([53c9947](https://github.com/k8sgpt-ai/k8sgpt/commit/53c994725ea2c2c54898ffe5307d9df40e9c1fe5)) * **main:** release 0.0.3 ([f5d8609](https://github.com/k8sgpt-ai/k8sgpt/commit/f5d86092f49faef8d71cb950986d76c3f92daf46)) * **main:** release 0.0.3 ([22873a6](https://github.com/k8sgpt-ai/k8sgpt/commit/22873a67163e58484d2a0ad343b4ba3c83e51d8f)) * **main:** release 0.0.4 ([6dbcde9](https://github.com/k8sgpt-ai/k8sgpt/commit/6dbcde94e961a6e5a1fc0559d2a1da5567a659de)) * release 0.0.3 ([4840aa0](https://github.com/k8sgpt-ai/k8sgpt/commit/4840aa081e3aa4a7a01fd3fd5f837fa6f0c3c02c)) * release 0.0.3 ([de02795](https://github.com/k8sgpt-ai/k8sgpt/commit/de027955ea18a751c5f991e7ff0f60b90ae704b0)) * release 0.0.3 ([a927c32](https://github.com/k8sgpt-ai/k8sgpt/commit/a927c32def806bb8b99e1cfcd4ee3dcdeca6ae5d)) * release 0.0.4 ([08f2c31](https://github.com/k8sgpt-ai/k8sgpt/commit/08f2c3112e2cc16b49b9cf8fdbd97368acecc754)) ## [0.0.4](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.0.3...v0.0.4) (2023-03-24) ### Bug Fixes * **deps:** update module github.com/sashabaranov/go-openai to v1.5.7 ([7f7726d](https://github.com/k8sgpt-ai/k8sgpt/commit/7f7726d59a63baeaf8ff110e00b30a20ec7f1df5)) ### Docs * add some important information to contributing ([9ab7f58](https://github.com/k8sgpt-ai/k8sgpt/commit/9ab7f587620d69e4e8fc98faabce6417c35f7497)) * update CONTRIBUTING ([05a787d](https://github.com/k8sgpt-ai/k8sgpt/commit/05a787d53dfe5e625c6449ac1e21ec36e66ddd28)) * update CONTRIBUTING ([26449e1](https://github.com/k8sgpt-ai/k8sgpt/commit/26449e10efd8926cccd4a2eaa4e9dc3afa8bd01a)) ### Other * add bot secret to goreleaser ([171e58b](https://github.com/k8sgpt-ai/k8sgpt/commit/171e58b51107f75717694e35c4e249ee41f0409a)) * add brew tap generation on release ([2992c4e](https://github.com/k8sgpt-ai/k8sgpt/commit/2992c4e5c8abad50c90ed85523c732f19ab1f31c)) * **deps:** update actions/checkout digest to 8f4b7f8 ([9955d75](https://github.com/k8sgpt-ai/k8sgpt/commit/9955d754505b60f28d17397132a1d02e95ffe303)) ## [0.0.3](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.0.3...v0.0.3) (2023-03-23) ### Features * build container ([260640f](https://github.com/k8sgpt-ai/k8sgpt/commit/260640f865baefba8ac256f800d4992f25ca15fd)) * find replicaset errors ([8ac56e0](https://github.com/k8sgpt-ai/k8sgpt/commit/8ac56e062baef2a0cf7c7ce2b4c97753f079f157)) ### Bug Fixes * add permissions to read repository ([d6cc4cf](https://github.com/k8sgpt-ai/k8sgpt/commit/d6cc4cfcbffbf84f27c7e4e4159da1e42dd5d689)) * build ([1fbed3e](https://github.com/k8sgpt-ai/k8sgpt/commit/1fbed3e44ff790fccfef502ddafae92e34629c21)) * container naming ([115276e](https://github.com/k8sgpt-ai/k8sgpt/commit/115276e01a38fc1692d6b66ab56a33f1e1793974)) * **deps:** update module github.com/sashabaranov/go-openai to v1.5.5 ([105fe44](https://github.com/k8sgpt-ai/k8sgpt/commit/105fe44680e5a987d4a65ff9c58b5b2211808c5e)) * **deps:** update module github.com/sashabaranov/go-openai to v1.5.6 ([37a1d3f](https://github.com/k8sgpt-ai/k8sgpt/commit/37a1d3f47e07caddb168f228627973870a9d867e)) * minor adaptions ([ef17b84](https://github.com/k8sgpt-ai/k8sgpt/commit/ef17b845ba3c65c16ed5dcc417e3e3d3d40dd04e)) * release please config ([c402c7b](https://github.com/k8sgpt-ai/k8sgpt/commit/c402c7bab7baababbbc7c82965d8337de7d50d35)) ### Other * add initial renovate config ([e37dbc7](https://github.com/k8sgpt-ai/k8sgpt/commit/e37dbc7909f1c520c4c6660c25b45de5847ea581)) * add pull request template ([a6d5132](https://github.com/k8sgpt-ai/k8sgpt/commit/a6d5132b8c2ff077680e2edfd8361a93008197fd)) * add release-please ([da7b409](https://github.com/k8sgpt-ai/k8sgpt/commit/da7b40978d55a6afed4c3a1ca83a756238feaca8)) * change module repo ([a307c13](https://github.com/k8sgpt-ai/k8sgpt/commit/a307c132b3464ff2e949c8a5588e01d344de91a0)) * **deps:** pin dependencies ([5b360de](https://github.com/k8sgpt-ai/k8sgpt/commit/5b360de2ae6094cf850a4ae973a22855c21a9040)) * **deps:** pin dependencies ([7fea7d1](https://github.com/k8sgpt-ai/k8sgpt/commit/7fea7d14a572fe0fd05f5f241b98e93655fb1965)) * **main:** release 0.0.3 ([f5d8609](https://github.com/k8sgpt-ai/k8sgpt/commit/f5d86092f49faef8d71cb950986d76c3f92daf46)) * **main:** release 0.0.3 ([22873a6](https://github.com/k8sgpt-ai/k8sgpt/commit/22873a67163e58484d2a0ad343b4ba3c83e51d8f)) * release 0.0.3 ([4840aa0](https://github.com/k8sgpt-ai/k8sgpt/commit/4840aa081e3aa4a7a01fd3fd5f837fa6f0c3c02c)) * release 0.0.3 ([de02795](https://github.com/k8sgpt-ai/k8sgpt/commit/de027955ea18a751c5f991e7ff0f60b90ae704b0)) * release 0.0.3 ([a927c32](https://github.com/k8sgpt-ai/k8sgpt/commit/a927c32def806bb8b99e1cfcd4ee3dcdeca6ae5d)) ## [0.0.3](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.0.3...v0.0.3) (2023-03-23) ### Other * release 0.0.3 ([de02795](https://github.com/k8sgpt-ai/k8sgpt/commit/de027955ea18a751c5f991e7ff0f60b90ae704b0)) * release 0.0.3 ([a927c32](https://github.com/k8sgpt-ai/k8sgpt/commit/a927c32def806bb8b99e1cfcd4ee3dcdeca6ae5d)) ## [0.0.3](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.0.2...v0.0.3) (2023-03-23) ### Features * build container ([260640f](https://github.com/k8sgpt-ai/k8sgpt/commit/260640f865baefba8ac256f800d4992f25ca15fd)) * find replicaset errors ([8ac56e0](https://github.com/k8sgpt-ai/k8sgpt/commit/8ac56e062baef2a0cf7c7ce2b4c97753f079f157)) ### Bug Fixes * add permissions to read repository ([d6cc4cf](https://github.com/k8sgpt-ai/k8sgpt/commit/d6cc4cfcbffbf84f27c7e4e4159da1e42dd5d689)) * build ([1fbed3e](https://github.com/k8sgpt-ai/k8sgpt/commit/1fbed3e44ff790fccfef502ddafae92e34629c21)) * container naming ([115276e](https://github.com/k8sgpt-ai/k8sgpt/commit/115276e01a38fc1692d6b66ab56a33f1e1793974)) * **deps:** update module github.com/sashabaranov/go-openai to v1.5.6 ([37a1d3f](https://github.com/k8sgpt-ai/k8sgpt/commit/37a1d3f47e07caddb168f228627973870a9d867e)) * minor adaptions ([ef17b84](https://github.com/k8sgpt-ai/k8sgpt/commit/ef17b845ba3c65c16ed5dcc417e3e3d3d40dd04e)) * release please config ([c402c7b](https://github.com/k8sgpt-ai/k8sgpt/commit/c402c7bab7baababbbc7c82965d8337de7d50d35)) ### Other * add release-please ([da7b409](https://github.com/k8sgpt-ai/k8sgpt/commit/da7b40978d55a6afed4c3a1ca83a756238feaca8)) * **deps:** pin dependencies ([5b360de](https://github.com/k8sgpt-ai/k8sgpt/commit/5b360de2ae6094cf850a4ae973a22855c21a9040)) ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at contact@k8sgpt.ai. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing We're happy that you want to contribute to this project. Please read the sections to make the process as smooth as possible. ## Requirements - Golang `1.24+` - An OpenAI API key * OpenAI API keys can be obtained from [OpenAI](https://platform.openai.com/account/api-keys) * You can set the API key for k8sgpt using `./k8sgpt auth key` - If you want to build the container image, you need to have a container engine (docker, podman, rancher, etc.) installed ## Getting Started **Where should I start?** - If you are new to the project, please check out the [good first issue](https://github.com/k8sgpt-ai/k8sgpt/labels/good%20first%20issue) label. - If you are looking for something to work on, check out our [open issues](https://github.com/k8sgpt-ai/k8sgpt/issues). - If you have an idea for a new feature, please open an issue, and we can discuss it. - We are also happy to help you find something to work on. Just reach out to us. **Getting in touch with the community** * Join our [#k8sgpt slack channel](https://join.slack.com/t/k8sgpt/shared_invite/zt-1rwe5fpzq-VNtJK8DmYbbm~iWL1H34nw) * Introduce yourself on the slack channel or open an issue to let us know that you are interested in contributing **Discuss issues** * Before you start working on something, propose and discuss your solution on the issue * If you are unsure about something, ask the community **How do I contribute?** - Fork the repository and clone it locally - Create a new branch and follow [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) guidelines for work undertaken - Assign yourself to the issue, if you are working on it (if you are not a member of the organization, please leave a comment on the issue) - Make your changes - Keep pull requests small and focused, if you have multiple changes, please open multiple PRs - Create a pull request back to the upstream repository and follow the [pull request template](.github/pull_request_template.md) guidelines. - Wait for a review and address any comments **Opening PRs** - As long as you are working on your PR, please mark it as a draft - Please make sure that your PR is up-to-date with the latest changes in `main` - Fill out the PR template - Mention the issue that your PR is addressing (closes: #) - Make sure that your PR passes all checks **Reviewing PRs** - Be respectful and constructive - Assign yourself to the PR - Check if all checks are passing - Suggest changes instead of simply commenting on found issues - If you are unsure about something, ask the author - If you are not sure if the changes work, try them out - Reach out to other reviewers if you are unsure about something - If you are happy with the changes, approve the PR - Merge the PR once it has all approvals and the checks are passing ## DCO We have a DCO check which runs on every PR to verify that the commit has been signed off. To sign off the last commit you made, you can use ``` git commit --amend --signoff ``` You can also automate signing off your commits by adding the following to your `.zshrc` or `.bashrc`: ``` git() { if [ $# -gt 0 ] && [[ "$1" == "commit" ]] ; then shift command git commit --signoff "$@" else command git "$@" fi } ``` ## Semantic commits We use [Semantic Commits](https://www.conventionalcommits.org/en/v1.0.0/) to make it easier to understand what a commit does and to build pretty changelogs. Please use the following prefixes for your commits: - `feat`: A new feature - `fix`: A bug fix - `docs`: Documentation changes - `chores`: Changes to the build process or auxiliary tools and libraries such as documentation generation - `refactor`: A code change that neither fixes a bug nor adds a feature - `test`: Adding missing tests or correcting existing tests - `ci`: Changes to our CI configuration files and scripts An example for this could be: ``` git commit -m "docs: add a new section to the README" ``` ## Building Building the binary is as simple as running `go build .` in the root of the repository. If you want to build the container image, you can run `docker build -t k8sgpt -f container/Dockerfile .` in the root of the repository. ## Releasing Releases of k8sgpt are done using [Release Please](https://github.com/googleapis/release-please) and [GoReleaser](https://goreleaser.com/). The workflow looks like this: * A PR is merged to the `main` branch: * Release please is triggered, creates or updates a new release PR * This is done with every merge to main, the current release PR is updated every time * Merging the 'release please' PR to `main`: * Release please is triggered, creates a new release and updates the changelog based on the commit messages * GoReleaser is triggered, builds the binaries and attaches them to the release * Containers are created and pushed to the container registry > With the next relevant merge, a new release PR will be created and the process starts again ### Manually setting the version If you want to manually set the version, you can create a PR with an empty commit message that contains the version number in the commit message. For example: Such a commit can get produced as follows: `git commit --allow-empty -m "chore: release 0.0.3" -m "Release-As: 0.0.3` ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2023 The k8sgpt Authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: MCP.md ================================================ # K8sGPT Model Context Protocol (MCP) Server K8sGPT provides a Model Context Protocol (MCP) server that exposes Kubernetes cluster operations as standardized tools, resources, and prompts for AI assistants like Claude, ChatGPT, and other MCP-compatible clients. ## Table of Contents - [What is MCP?](#what-is-mcp) - [Quick Start](#quick-start) - [Server Modes](#server-modes) - [Available Tools](#available-tools) - [Available Resources](#available-resources) - [Available Prompts](#available-prompts) - [Usage Examples](#usage-examples) - [Integration with AI Assistants](#integration-with-ai-assistants) - [HTTP API Reference](#http-api-reference) ## What is MCP? The Model Context Protocol (MCP) is an open standard that enables AI assistants to securely connect to external data sources and tools. K8sGPT's MCP server exposes Kubernetes operations through this standardized interface, allowing AI assistants to: - Analyze cluster health and issues - Query Kubernetes resources - Access pod logs and events - Get troubleshooting guidance - Manage analyzer filters ## Quick Start ### Start the MCP Server **Stdio mode (for local AI assistants):** ```bash k8sgpt serve --mcp ``` **HTTP mode (for network access):** ```bash k8sgpt serve --mcp --mcp-http --mcp-port 8089 ``` ### Test with curl ```bash curl -X POST http://localhost:8089/mcp \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/list" }' ``` ## Server Modes ### Stdio Mode (Default) Used by local AI assistants like Claude Desktop: ```bash k8sgpt serve --mcp ``` Configure in your MCP client (e.g., Claude Desktop's `claude_desktop_config.json`): ```json { "mcpServers": { "k8sgpt": { "command": "k8sgpt", "args": ["serve", "--mcp"] } } } ``` ### HTTP Mode Used for network access and webhooks: ```bash k8sgpt serve --mcp --mcp-http --mcp-port 8089 ``` The server runs in stateless mode, so no session management is required. Each request is independent. ## Available Tools The MCP server exposes 12 tools for Kubernetes operations: ### Cluster Analysis **analyze** - Analyze Kubernetes resources for issues and problems - Parameters: - `namespace` (optional): Namespace to analyze - `explain` (optional): Get AI explanations for issues - `filters` (optional): Comma-separated list of analyzers to use **cluster-info** - Get Kubernetes cluster information and version ### Resource Management **list-resources** - List Kubernetes resources of a specific type - Parameters: - `resourceType` (required): Type of resource (pods, deployments, services, nodes, jobs, cronjobs, statefulsets, daemonsets, replicasets, configmaps, secrets, ingresses, pvcs, pvs) - `namespace` (optional): Namespace to query - `labelSelector` (optional): Label selector for filtering **get-resource** - Get detailed information about a specific Kubernetes resource - Parameters: - `resourceType` (required): Type of resource - `name` (required): Resource name - `namespace` (optional): Namespace **list-namespaces** - List all namespaces in the cluster ### Debugging and Troubleshooting **get-logs** - Get logs from a pod container - Parameters: - `podName` (required): Name of the pod - `namespace` (optional): Namespace - `container` (optional): Container name - `tail` (optional): Number of lines to show - `previous` (optional): Show logs from previous container instance - `sinceSeconds` (optional): Show logs from last N seconds **list-events** - List Kubernetes events for debugging - Parameters: - `namespace` (optional): Namespace to query - `involvedObjectName` (optional): Filter by object name - `involvedObjectKind` (optional): Filter by object kind ### Analyzer Management **list-filters** - List all available and active analyzers/filters **add-filters** - Add filters to enable specific analyzers - Parameters: - `filters` (required): Comma-separated list of analyzer names **remove-filters** - Remove filters to disable specific analyzers - Parameters: - `filters` (required): Comma-separated list of analyzer names ### Integrations **list-integrations** - List available integrations (Prometheus, AWS, Keda, Kyverno, etc.) ### Configuration **config** - Configure K8sGPT settings including custom analyzers and cache ## Available Resources Resources provide read-only access to cluster information: **cluster-info** - URI: `cluster-info` - Get information about the Kubernetes cluster **namespaces** - URI: `namespaces` - List all namespaces in the cluster **active-filters** - URI: `active-filters` - Get currently active analyzers/filters ## Available Prompts Prompts provide guided troubleshooting workflows: **troubleshoot-pod** - Interactive pod debugging workflow - Arguments: - `podName` (required): Name of the pod to troubleshoot - `namespace` (required): Namespace of the pod **troubleshoot-deployment** - Interactive deployment debugging workflow - Arguments: - `deploymentName` (required): Name of the deployment - `namespace` (required): Namespace of the deployment **troubleshoot-cluster** - General cluster troubleshooting workflow ## Usage Examples ### Example 1: Analyze a Namespace ```bash curl -X POST http://localhost:8089/mcp \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "analyze", "arguments": { "namespace": "production", "explain": "true" } } }' ``` ### Example 2: List Pods ```bash curl -X POST http://localhost:8089/mcp \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "list-resources", "arguments": { "resourceType": "pods", "namespace": "default" } } }' ``` ### Example 3: Get Pod Logs ```bash curl -X POST http://localhost:8089/mcp \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "get-logs", "arguments": { "podName": "nginx-abc123", "namespace": "default", "tail": "100" } } }' ``` ### Example 4: Access a Resource ```bash curl -X POST http://localhost:8089/mcp \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "id": 4, "method": "resources/read", "params": { "uri": "namespaces" } }' ``` ### Example 5: Get a Troubleshooting Prompt ```bash curl -X POST http://localhost:8089/mcp \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "id": 5, "method": "prompts/get", "params": { "name": "troubleshoot-pod", "arguments": { "podName": "nginx-abc123", "namespace": "default" } } }' ``` ## Integration with AI Assistants ### Claude Desktop Add to `claude_desktop_config.json`: ```json { "mcpServers": { "k8sgpt": { "command": "k8sgpt", "args": ["serve", "--mcp"] } } } ``` Restart Claude Desktop and you'll see k8sgpt tools available in the tool selector. ### Custom MCP Clients Any MCP-compatible client can connect to the k8sgpt server. For HTTP-based clients: 1. Start the server: `k8sgpt serve --mcp --mcp-http --mcp-port 8089` 2. Connect to: `http://localhost:8089/mcp` 3. Use standard MCP protocol methods: `tools/list`, `tools/call`, `resources/read`, `prompts/get` ## HTTP API Reference ### Endpoint ``` POST http://localhost:8089/mcp Content-Type: application/json ``` ### Request Format All requests follow the JSON-RPC 2.0 format: ```json { "jsonrpc": "2.0", "id": 1, "method": "method_name", "params": { ... } } ``` ### Discovery Methods **List Tools** ```json {"jsonrpc": "2.0", "id": 1, "method": "tools/list"} ``` **List Resources** ```json {"jsonrpc": "2.0", "id": 2, "method": "resources/list"} ``` **List Prompts** ```json {"jsonrpc": "2.0", "id": 3, "method": "prompts/list"} ``` ### Tool Invocation ```json { "jsonrpc": "2.0", "id": 4, "method": "tools/call", "params": { "name": "tool_name", "arguments": { "arg1": "value1", "arg2": "value2" } } } ``` ### Resource Access ```json { "jsonrpc": "2.0", "id": 5, "method": "resources/read", "params": { "uri": "resource_uri" } } ``` ### Prompt Access ```json { "jsonrpc": "2.0", "id": 6, "method": "prompts/get", "params": { "name": "prompt_name", "arguments": { "arg1": "value1" } } } ``` ### Response Format Successful responses: ```json { "jsonrpc": "2.0", "id": 1, "result": { ... } } ``` Error responses: ```json { "jsonrpc": "2.0", "id": 1, "error": { "code": -32600, "message": "Error description" } } ``` ## Advanced Configuration ### Custom Port ```bash k8sgpt serve --mcp --mcp-http --mcp-port 9000 ``` ### With Specific Backend ```bash k8sgpt serve --mcp --backend openai ``` ### With Kubeconfig ```bash k8sgpt serve --mcp --kubeconfig ~/.kube/config ``` ## Troubleshooting ### Connection Issues Verify the server is running: ```bash curl http://localhost:8089/mcp ``` ### Permission Issues Ensure your kubeconfig has appropriate cluster access: ```bash kubectl cluster-info ``` ### Tool Errors List available tools to verify names: ```bash curl -X POST http://localhost:8089/mcp \ -H "Content-Type: application/json" \ -d '{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}' ``` ## Learn More - [MCP Specification](https://modelcontextprotocol.io/) - [K8sGPT Documentation](https://docs.k8sgpt.ai/) - [MCP Go Library](https://github.com/mark3labs/mcp-go) ================================================ FILE: Makefile ================================================ # Copyright 2023 K8sgpt AI. All rights reserved. # Use of this source code is governed by a MIT style # license that can be found in the LICENSE file. # ============================================================================== # define the default goal # ROOT_PACKAGE=github.com/k8sgpt-ai/k8sgpt SHELL := /bin/bash DIRS=$(shell ls) GO=go GOOS ?= $(shell go env GOOS) GOARCH ?= $(shell go env GOARCH) .DEFAULT_GOAL := help # include the common makefile COMMON_SELF_DIR := $(dir $(lastword $(MAKEFILE_LIST))) # ROOT_DIR: root directory of the code base ifeq ($(origin ROOT_DIR),undefined) ROOT_DIR := $(abspath $(shell cd $(COMMON_SELF_DIR)/. && pwd -P)) endif # OUTPUT_DIR: The directory where the build output is stored. ifeq ($(origin OUTPUT_DIR),undefined) OUTPUT_DIR := $(ROOT_DIR)/bin $(shell mkdir -p $(OUTPUT_DIR)) endif ifeq ($(origin VERSION), undefined) VERSION := $(shell git describe --abbrev=0 --dirty --always --tags | sed 's/-/./g') endif # Check if the tree is dirty. default to dirty(maybe u should commit?) GIT_TREE_STATE:="dirty" ifeq (, $(shell git status --porcelain 2>/dev/null)) GIT_TREE_STATE="clean" endif GIT_COMMIT:=$(shell git rev-parse HEAD) IMG ?= ghcr.io/k8sgpt-ai/k8sgpt:latest BUILDFILE = "./main.go" BUILDAPP = "$(OUTPUT_DIR)/k8sgpt" .PHONY: all all: tidy add-copyright lint cover build # ============================================================================== # Targets ## build: Build binaries by default .PHONY: build build: @echo "$(shell go version)" @echo "===========> Building binary $(BUILDAPP) *[Git Info]: $(VERSION)-$(GIT_COMMIT)" @export CGO_ENABLED=0 && go build -o $(BUILDAPP) -ldflags "-s -w -X main.version=dev -X main.commit=$$(git rev-parse --short HEAD) -X main.date=$$(date +%FT%TZ)" $(BUILDFILE) ## tidy: tidy go.mod .PHONY: tidy tidy: @$(GO) mod tidy ## deploy: Deploy k8sgpt .PHONY: deploy deploy: helm @echo "===========> Deploying k8sgpt" $(HELM) install k8sgpt charts/k8sgpt -n k8sgpt --create-namespace ## update: Update k8sgpt .PHONY: update update: helm @echo "===========> Updating k8sgpt" $(HELM) upgrade k8sgpt charts/k8sgpt -n k8sgpt ## undeploy: Undeploy k8sgpt .PHONY: undeploy undeploy: helm @echo "===========> Undeploying k8sgpt" $(HELM) uninstall k8sgpt -n k8sgpt ## docker-build: Build docker image .PHONY: docker-build docker-build: @echo "===========> Building docker image" docker buildx build --build-arg=VERSION="$$(git describe --tags --abbrev=0)" --build-arg=COMMIT="$$(git rev-parse --short HEAD)" --build-arg DATE="$$(date +%FT%TZ)" --platform="linux/amd64,linux/arm64" -t ${IMG} -f container/Dockerfile . --push ## docker-build-local: Build docker image for local testing .PHONY: docker-build-local docker-build-local: @echo "===========> Building docker image for local testing" docker build --build-arg=VERSION="$$(git describe --tags --abbrev=0)" --build-arg=COMMIT="$$(git rev-parse --short HEAD)" --build-arg DATE="$$(date +%FT%TZ)" -t k8sgpt:local -f container/Dockerfile . ## fmt: Run go fmt against code. .PHONY: fmt fmt: @$(GO) fmt ./... ## vet: Run go vet against code. .PHONY: vet vet: @$(GO) vet ./... ## lint: Run go lint against code. .PHONY: lint lint: @golangci-lint run -v --timeout=5m ./... ## style: Code style -> fmt,vet,lint .PHONY: style style: fmt vet lint ## test: Run unit test .PHONY: test test: @echo "===========> Run unit test" @$(GO) test ./... ## cover: Run unit test with coverage .PHONY: cover cover: test @$(GO) test -cover ## go.clean: Clean all builds .PHONY: clean clean: @echo "===========> Cleaning all builds OUTPUT_DIR($(OUTPUT_DIR))" @-rm -vrf $(OUTPUT_DIR) @echo "===========> End clean..." ## help: Show this help info. .PHONY: help help: Makefile @printf "\n\033[1mUsage: make ...\033[0m\n\n\\033[1mTargets:\\033[0m\n\n" @sed -n 's/^##//p' $< | awk -F':' '{printf "\033[36m%-28s\033[0m %s\n", $$1, $$2}' | sed -e 's/^/ /' ## copyright.verify: Validate boilerplate headers for assign files .PHONY: copyright.verify copyright.verify: tools.verify.addlicense @echo "===========> Validate boilerplate headers for assign files starting in the $(ROOT_DIR) directory" # @addlicense -v -check -ignore **/test/** -f $(LICENSE_TEMPLATE) $(CODE_DIRS) @echo "===========> End of boilerplate headers check..." ## copyright.add: Add the boilerplate headers for all files .PHONY: copyright.add copyright.add: tools.verify.addlicense @echo "===========> Adding $(LICENSE_TEMPLATE) the boilerplate headers for all files" # @addlicense -y $(shell date +"%Y") -v -c "K8sgpt AI." -f $(LICENSE_TEMPLATE) $(CODE_DIRS) @echo "===========> End the copyright is added..." # ===== # Tools HELM_VERSION ?= v3.11.3 helm: if ! test -f $(OUTPUT_DIR)/helm-$(GOOS)-$(GOARCH); then \ curl -L https://get.helm.sh/helm-$(HELM_VERSION)-$(GOOS)-$(GOARCH).tar.gz | tar xz; \ mv $(GOOS)-$(GOARCH)/helm $(OUTPUT_DIR)/helm-$(GOOS)-$(GOARCH); \ chmod +x $(OUTPUT_DIR)/helm-$(GOOS)-$(GOARCH); \ rm -rf ./$(GOOS)-$(GOARCH)/; \ fi HELM=$(OUTPUT_DIR)/helm-$(GOOS)-$(GOARCH) ================================================ FILE: README.md ================================================ Text changing depending on mode. Light: 'So light!' Dark: 'So dark!'
![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/k8sgpt-ai/k8sgpt) ![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/k8sgpt-ai/k8sgpt/release.yaml) ![GitHub release (latest by date)](https://img.shields.io/github/v/release/k8sgpt-ai/k8sgpt) [![OpenSSF Best Practices](https://bestpractices.coreinfrastructure.org/projects/7272/badge)](https://bestpractices.coreinfrastructure.org/projects/7272) [![Link to documentation](https://img.shields.io/static/v1?label=%F0%9F%93%96&message=Documentation&color=blue)](https://docs.k8sgpt.ai/) [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fk8sgpt-ai%2Fk8sgpt.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fk8sgpt-ai%2Fk8sgpt?ref=badge_shield) [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) [![Go version](https://img.shields.io/github/go-mod/go-version/k8sgpt-ai/k8sgpt.svg)](https://github.com/k8sgpt-ai/k8sgpt) [![codecov](https://codecov.io/github/k8sgpt-ai/k8sgpt/graph/badge.svg?token=ZLR7NG8URE)](https://codecov.io/github/k8sgpt-ai/k8sgpt) ![GitHub last commit (branch)](https://img.shields.io/github/last-commit/k8sgpt-ai/k8sgpt/main) `k8sgpt` is a tool for scanning your Kubernetes clusters, diagnosing, and triaging issues in simple English. It has SRE experience codified into its analyzers and helps to pull out the most relevant information to enrich it with AI. _Out of the box integration with OpenAI, Azure, Cohere, Amazon Bedrock, Google Gemini and local models._ > **Sister project:** Check out [sympozium](https://github.com/AlexsJones/sympozium/) for managing agents in Kubernetes. K8sGPT - K8sGPT gives Kubernetes Superpowers to everyone | Product Hunt Featured|HelloGitHub # Table of Contents - [Overview](#k8sgpt) - [Installation](#cli-installation) - [Quick Start](#quick-start) - [Analyzers](#analyzers) - [Examples](#examples) - [LLM AI Backends](#llm-ai-backends) - [Key Features](#key-features) - [Model Context Protocol (MCP)](#model-context-protocol-mcp) - [Documentation](#documentation) - [Contributing](#contributing) - [Community](#community) - [License](#license) # CLI Installation ### Linux/Mac via brew ```sh brew install k8sgpt ``` or ```sh brew tap k8sgpt-ai/k8sgpt brew install k8sgpt ```
RPM-based installation (RedHat/CentOS/Fedora) **32 bit:** ``` sudo rpm -ivh https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.30/k8sgpt_386.rpm ``` **64 bit:** ``` sudo rpm -ivh https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.30/k8sgpt_amd64.rpm ```
DEB-based installation (Ubuntu/Debian) **32 bit:** ``` curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.30/k8sgpt_386.deb sudo dpkg -i k8sgpt_386.deb ``` **64 bit:** ``` curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.30/k8sgpt_amd64.deb sudo dpkg -i k8sgpt_amd64.deb ```
APK-based installation (Alpine) **32 bit:** ``` wget https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.30/k8sgpt_386.apk apk add --allow-untrusted k8sgpt_386.apk ``` **64 bit:** ``` wget https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.30/k8sgpt_amd64.apk apk add --allow-untrusted k8sgpt_amd64.apk ```
Failing Installation on WSL or Linux (missing gcc) When installing Homebrew on WSL or Linux, you may encounter the following error: ``` ==> Installing k8sgpt from k8sgpt-ai/k8sgpt Error: The following formula cannot be installed from a bottle and must be built from the source. k8sgpt Install Clang or run brew install gcc. ``` If you install gcc as suggested, the problem will persist. Therefore, you need to install the build-essential package. ``` sudo apt-get update sudo apt-get install build-essential ```
### Windows - Download the latest Windows binaries of **k8sgpt** from the [Release](https://github.com/k8sgpt-ai/k8sgpt/releases) tab based on your system architecture. - Extract the downloaded package to your desired location. Configure the system _PATH_ environment variable with the binary location ## Operator Installation To install within a Kubernetes cluster please use our `k8sgpt-operator` with installation instructions available [here](https://github.com/k8sgpt-ai/k8sgpt-operator) _This mode of operation is ideal for continuous monitoring of your cluster and can integrate with your existing monitoring such as Prometheus and Alertmanager._ ## Quick Start - Currently, the default AI provider is OpenAI, you will need to generate an API key from [OpenAI](https://openai.com) - You can do this by running `k8sgpt generate` to open a browser link to generate it - Run `k8sgpt auth add` to set it in k8sgpt. - You can provide the password directly using the `--password` flag. - Run `k8sgpt filters` to manage the active filters used by the analyzer. By default, all filters are executed during analysis. - Run `k8sgpt analyze` to run a scan. - And use `k8sgpt analyze --explain` to get a more detailed explanation of the issues. - You also run `k8sgpt analyze --with-doc` (with or without the explain flag) to get the official documentation from Kubernetes. # Using with Claude Desktop K8sGPT can be integrated with Claude Desktop to provide AI-powered Kubernetes cluster analysis. This integration requires K8sGPT v0.4.14 or later. ## Prerequisites 1. Install K8sGPT v0.4.14 or later: ```sh brew install k8sgpt ``` 2. Install Claude Desktop from the official website 3. Configure K8sGPT with your preferred AI backend: ```sh k8sgpt auth ``` ## Setup 1. Start the K8sGPT MCP server: ```sh k8sgpt serve --mcp ``` 2. In Claude Desktop: - Open Settings - Navigate to the Integrations section - Add K8sGPT as a new integration - The MCP server will be automatically detected 3. Configure Claude Desktop with the following JSON: ```json { "mcpServers": { "k8sgpt": { "command": "k8sgpt", "args": [ "serve", "--mcp" ] } } } ``` ## Usage Once connected, you can use Claude Desktop to: - Analyze your Kubernetes cluster - Get detailed insights about cluster health - Receive recommendations for fixing issues - Query cluster information Example commands in Claude Desktop: - "Analyze my Kubernetes cluster" - "What's the health status of my cluster?" - "Show me any issues in the default namespace" ## Troubleshooting If you encounter connection issues: 1. Ensure K8sGPT is running with the MCP server enabled 2. Verify your Kubernetes cluster is accessible 3. Check that your AI backend is properly configured 4. Restart both K8sGPT and Claude Desktop For more information, visit our [documentation](https://docs.k8sgpt.ai). ## Analyzers K8sGPT uses analyzers to triage and diagnose issues in your cluster. It has a set of analyzers that are built in, but you will be able to write your own analyzers. ### Built in analyzers #### Enabled by default - [x] podAnalyzer - [x] pvcAnalyzer - [x] rsAnalyzer - [x] serviceAnalyzer - [x] eventAnalyzer - [x] ingressAnalyzer - [x] statefulSetAnalyzer - [x] deploymentAnalyzer - [x] jobAnalyzer - [x] cronJobAnalyzer - [x] nodeAnalyzer - [x] mutatingWebhookAnalyzer - [x] validatingWebhookAnalyzer - [x] configMapAnalyzer #### Optional - [x] hpaAnalyzer - [x] pdbAnalyzer - [x] networkPolicyAnalyzer - [x] gatewayClass - [x] gateway - [x] httproute - [x] logAnalyzer - [x] storageAnalyzer - [x] securityAnalyzer - [x] CatalogSource - [x] ClusterCatalog - [x] ClusterExtension - [x] ClusterService - [x] ClusterServiceVersion - [x] OperatorGroup - [x] InstallPlan - [x] Subscription ## Examples _Run a scan with the default analyzers_ ``` k8sgpt generate k8sgpt auth add k8sgpt analyze --explain k8sgpt analyze --explain --with-doc ``` _Filter on resource_ ``` k8sgpt analyze --explain --filter=Service ``` _Filter by namespace_ ``` k8sgpt analyze --explain --filter=Pod --namespace=default ``` _Output to JSON_ ``` k8sgpt analyze --explain --filter=Service --output=json ``` _Anonymize during explain_ ``` k8sgpt analyze --explain --filter=Service --output=json --anonymize ```
Using filters _List filters_ ``` k8sgpt filters list ``` _Add default filters_ ``` k8sgpt filters add [filter(s)] ``` ### Examples : - Simple filter : `k8sgpt filters add Service` - Multiple filters : `k8sgpt filters add Ingress,Pod` _Remove default filters_ ``` k8sgpt filters remove [filter(s)] ``` ### Examples : - Simple filter : `k8sgpt filters remove Service` - Multiple filters : `k8sgpt filters remove Ingress,Pod`
Additional commands _List configured backends_ ``` k8sgpt auth list ``` _Update configured backends_ ``` k8sgpt auth update $MY_BACKEND1,$MY_BACKEND2.. ``` _Remove configured backends_ ``` k8sgpt auth remove -b $MY_BACKEND1,$MY_BACKEND2.. ``` _List integrations_ ``` k8sgpt integrations list ``` _Activate integrations_ ``` k8sgpt integrations activate [integration(s)] ``` _Use integration_ ``` k8sgpt analyze --filter=[integration(s)] ``` _Deactivate integrations_ ``` k8sgpt integrations deactivate [integration(s)] ``` _Serve mode_ ``` k8sgpt serve ``` _Serve mode with MCP (Model Context Protocol)_ ``` # Enable MCP server on default port 8089 k8sgpt serve --mcp --mcp-http # Enable MCP server on custom port k8sgpt serve --mcp --mcp-http --mcp-port 8089 # Full serve mode with MCP k8sgpt serve --mcp --mcp-http --port 8080 --metrics-port 8081 --mcp-port 8089 ``` The MCP server enables integration with tools like Claude Desktop and other MCP-compatible clients. It runs on port 8089 by default and provides: - Kubernetes cluster analysis via MCP protocol - Resource information and health status - AI-powered issue explanations and recommendations For Helm chart deployment with MCP support, see the `charts/k8sgpt/values-mcp-example.yaml` file. _Analysis with serve mode_ ``` grpcurl -plaintext -d '{"namespace": "k8sgpt", "explain" : "true"}' localhost:8080 schema.v1.ServerAnalyzerService/Analyze { "status": "OK" } ``` _Analysis with custom headers_ ``` k8sgpt analyze --explain --custom-headers CustomHeaderKey:CustomHeaderValue ``` _Print analysis stats_ ``` k8sgpt analyze -s The stats mode allows for debugging and understanding the time taken by an analysis by displaying the statistics of each analyzer. - Analyzer Ingress took 47.125583ms - Analyzer PersistentVolumeClaim took 53.009167ms - Analyzer CronJob took 57.517792ms - Analyzer Deployment took 156.6205ms - Analyzer Node took 160.109833ms - Analyzer ReplicaSet took 245.938333ms - Analyzer StatefulSet took 448.0455ms - Analyzer Pod took 5.662594708s - Analyzer Service took 38.583359166s ``` _Diagnostic information_ To collect diagnostic information use the following command to create a `dump__json` in your local directory. ``` k8sgpt dump ```
## LLM AI Backends K8sGPT uses the chosen LLM, generative AI provider when you want to explain the analysis results using --explain flag e.g. `k8sgpt analyze --explain`. You can use `--backend` flag to specify a configured provider (it's `openai` by default). You can list available providers using `k8sgpt auth list`: ``` Default: > openai Active: Unused: > openai > localai > ollama > azureopenai > cohere > amazonbedrock > amazonsagemaker > google > huggingface > noopai > googlevertexai > watsonxai > customrest > ibmwatsonxai ``` For detailed documentation on how to configure and use each provider see [here](https://docs.k8sgpt.ai/reference/providers/backend/). _To set a new default provider_ ``` k8sgpt auth default -p azureopenai Default provider set to azureopenai ``` _Using Amazon Bedrock with inference profiles_ _System Inference Profile_ ``` k8sgpt auth add --backend amazonbedrock --providerRegion us-east-1 --model arn:aws:bedrock:us-east-1:123456789012:inference-profile/my-inference-profile ``` _Application Inference Profile_ ``` k8sgpt auth add --backend amazonbedrock --providerRegion us-east-1 --model arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/2uzp4s0w39t6 ``` ## Key Features
With this option, the data is anonymized before being sent to the AI Backend. During the analysis execution, `k8sgpt` retrieves sensitive data (Kubernetes object names, labels, etc.). This data is masked when sent to the AI backend and replaced by a key that can be used to de-anonymize the data when the solution is returned to the user. Anonymization 1. Error reported during analysis: ```bash Error: HorizontalPodAutoscaler uses StatefulSet/fake-deployment as ScaleTargetRef which does not exist. ``` 2. Payload sent to the AI backend: ```bash Error: HorizontalPodAutoscaler uses StatefulSet/tGLcCRcHa1Ce5Rs as ScaleTargetRef which does not exist. ``` 3. Payload returned by the AI: ```bash The Kubernetes system is trying to scale a StatefulSet named tGLcCRcHa1Ce5Rs using the HorizontalPodAutoscaler, but it cannot find the StatefulSet. The solution is to verify that the StatefulSet name is spelled correctly and exists in the same namespace as the HorizontalPodAutoscaler. ``` 4. Payload returned to the user: ```bash The Kubernetes system is trying to scale a StatefulSet named fake-deployment using the HorizontalPodAutoscaler, but it cannot find the StatefulSet. The solution is to verify that the StatefulSet name is spelled correctly and exists in the same namespace as the HorizontalPodAutoscaler. ``` ### Further Details Note: **Anonymization does not currently apply to events.** _In a few analysers like Pod, we feed to the AI backend the event messages which are not known beforehand thus we are not masking them for the **time being**._ - The following is the list of analysers in which data is **being masked**:- - Statefulset - Service - PodDisruptionBudget - Node - NetworkPolicy - Ingress - HPA - Deployment - Cronjob - The following is the list of analysers in which data is **not being masked**:- - ReplicaSet - PersistentVolumeClaim - Pod - Log - **_\*Events_** **\*Note**: - k8gpt will not mask the above analysers because they do not send any identifying information except **Events** analyser. - Masking for **Events** analyzer is scheduled in the near future as seen in this [issue](https://github.com/k8sgpt-ai/k8sgpt/issues/560). _Further research has to be made to understand the patterns and be able to mask the sensitive parts of an event like pod name, namespace etc._ - The following is the list of fields which are not **being masked**:- - Describe - ObjectStatus - Replicas - ContainerStatus - **_\*Event Message_** - ReplicaStatus - Count (Pod) **\*Note**: - It is quite possible the payload of the event message might have something like "super-secret-project-pod-X crashed" which we don't currently redact _(scheduled in the near future as seen in this [issue](https://github.com/k8sgpt-ai/k8sgpt/issues/560))_. ### Proceed with care - The K8gpt team recommends using an entirely different backend **(a local model) in critical production environments**. By using a local model, you can rest assured that everything stays within your DMZ, and nothing is leaked. - If there is any uncertainty about the possibility of sending data to a public LLM (open AI, Azure AI) and it poses a risk to business-critical operations, then, in such cases, the use of public LLM should be avoided based on personal assessment and the jurisdiction of risks involved.
Configuration management `k8sgpt` stores config data in the `$XDG_CONFIG_HOME/k8sgpt/k8sgpt.yaml` file. The data is stored in plain text, including your OpenAI key. Config file locations: | OS | Path | | ------- | ------------------------------------------------ | | MacOS | ~/Library/Application Support/k8sgpt/k8sgpt.yaml | | Linux | ~/.config/k8sgpt/k8sgpt.yaml | | Windows | %LOCALAPPDATA%/k8sgpt/k8sgpt.yaml |
There may be scenarios where caching remotely is preferred. In these scenarios K8sGPT supports AWS S3 or Azure Blob storage Integration. Remote caching Note: You can configure and use only one remote cache at a time _Adding a remote cache_ - AWS S3 - _As a prerequisite `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` are required as environmental variables._ - Configuration, `k8sgpt cache add s3 --region --bucket ` - Minio Configuration with HTTP endpoint ` k8sgpt cache add s3 --bucket --endpoint ` - Minio Configuration with HTTPs endpoint, skipping TLS verification ` k8sgpt cache add s3 --bucket --endpoint --insecure` - K8sGPT will create the bucket if it does not exist - Azure Storage - We support a number of [techniques](https://learn.microsoft.com/en-us/azure/developer/go/azure-sdk-authentication?tabs=bash#2-authenticate-with-azure) to authenticate against Azure - Configuration, `k8sgpt cache add azure --storageacc --container ` - K8sGPT assumes that the storage account already exist and it will create the container if it does not exist - It is the **user** responsibility have to grant specific permissions to their identity in order to be able to upload blob files and create SA containers (e.g Storage Blob Data Contributor) - Google Cloud Storage - _As a prerequisite `GOOGLE_APPLICATION_CREDENTIALS` are required as environmental variables._ - Configuration, ` k8sgpt cache add gcs --region --bucket --projectid ` - K8sGPT will create the bucket if it does not exist _Listing cache items_ ``` k8sgpt cache list ``` _Purging an object from the cache_ Note: purging an object using this command will delete upstream files, so it requires appropriate permissions. ``` k8sgpt cache purge $OBJECT_NAME ``` _Removing the remote cache_ Note: this will not delete the upstream S3 bucket or Azure storage container ``` k8sgpt cache remove ```
Custom Analyzers There may be scenarios where you wish to write your own analyzer in a language of your choice. K8sGPT now supports the ability to do so by abiding by the [schema](https://github.com/k8sgpt-ai/schemas/blob/main/protobuf/schema/v1/custom_analyzer.proto) and serving the analyzer for consumption. To do so, define the analyzer within the K8sGPT configuration and it will add it into the scanning process. In addition to this you will need to enable the following flag on analysis: ``` k8sgpt analyze --custom-analysis ``` Here is an example local host analyzer in [Rust](https://github.com/k8sgpt-ai/host-analyzer) When this is run on `localhost:8080` the K8sGPT config can pick it up with the following additions: ``` custom_analyzers: - name: host-analyzer connection: url: localhost port: 8080 ``` This now gives the ability to pass through hostOS information ( from this analyzer example ) to K8sGPT to use as context with normal analysis. _See the docs on how to write a custom analyzer_ _Listing custom analyzers configured_ ``` k8sgpt custom-analyzer list ``` _Adding custom analyzer without install_ ``` k8sgpt custom-analyzer add --name my-custom-analyzer --port 8085 ``` _Removing custom analyzer_ ``` k8sgpt custom-analyzer remove --names "my-custom-analyzer,my-custom-analyzer-2" ```
## Model Context Protocol (MCP) K8sGPT provides a Model Context Protocol server that exposes Kubernetes operations as standardized tools for AI assistants like Claude, ChatGPT, and other MCP-compatible clients. **Start the MCP server:** Stdio mode (for local AI assistants): ```bash k8sgpt serve --mcp ``` HTTP mode (for network access): ```bash k8sgpt serve --mcp --mcp-http --mcp-port 8089 ``` **Features:** - 12 tools for cluster analysis, resource management, and debugging - 3 resources for cluster information access - 3 interactive troubleshooting prompts - Stateless HTTP mode for one-off invocations - Full integration with Claude Desktop and other MCP clients **Learn more:** See [MCP.md](MCP.md) for complete documentation, usage examples, and integration guides. ## Documentation Find our official documentation available [here](https://docs.k8sgpt.ai) ## Contributing Please read our [contributing guide](./CONTRIBUTING.md). ## Community Find us on [Slack](https://join.slack.com/t/k8sgpt/shared_invite/zt-332vhyaxv-bfjJwHZLXWVCB3QaXafEYQ) ## License [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fk8sgpt-ai%2Fk8sgpt.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fk8sgpt-ai%2Fk8sgpt?ref=badge_large) ================================================ FILE: SECURITY.md ================================================ # Security Policy ## Supported Versions We currently support the latest release for security patching and will deploy forward releases. 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` ## Reporting a Vulnerability 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. ================================================ FILE: SUPPORTED_MODELS.md ================================================ # Supported AI Providers and Models in K8sGPT 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. --- ## Providers and Supported Models ### OpenAI - **Model:** User-configurable (any model supported by OpenAI, e.g., `gpt-3.5-turbo`, `gpt-4`, etc.) ### Azure OpenAI - **Model:** User-configurable (any model deployed in your Azure OpenAI resource) ### LocalAI - **Model:** User-configurable (default: `llama3`) ### Ollama - **Model:** User-configurable (default: `llama3`, others can be specified) ### NoOpAI - **Model:** N/A (no real model, used for testing) ### Cohere - **Model:** User-configurable (any model supported by Cohere) ### Amazon Bedrock - **Supported Models:** - anthropic.claude-sonnet-4-20250514-v1:0 - us.anthropic.claude-sonnet-4-20250514-v1:0 - eu.anthropic.claude-sonnet-4-20250514-v1:0 - apac.anthropic.claude-sonnet-4-20250514-v1:0 - us.anthropic.claude-3-7-sonnet-20250219-v1:0 - eu.anthropic.claude-3-7-sonnet-20250219-v1:0 - apac.anthropic.claude-3-7-sonnet-20250219-v1:0 - anthropic.claude-3-5-sonnet-20240620-v1:0 - us.anthropic.claude-3-5-sonnet-20241022-v2:0 - anthropic.claude-v2 - anthropic.claude-v1 - anthropic.claude-instant-v1 - ai21.j2-ultra-v1 - ai21.j2-jumbo-instruct - amazon.titan-text-express-v1 - amazon.nova-pro-v1:0 - eu.amazon.nova-pro-v1:0 - us.amazon.nova-pro-v1:0 - amazon.nova-lite-v1:0 - eu.amazon.nova-lite-v1:0 - us.amazon.nova-lite-v1:0 - anthropic.claude-3-haiku-20240307-v1:0 > **Note:** > If you use an AWS Bedrock inference profile ARN (e.g., `arn:aws:bedrock:us-east-1::application-inference-profile/`) as the model, you must still provide a valid modelId (e.g., `anthropic.claude-3-sonnet-20240229-v1:0`). K8sGPT will automatically set the required `X-Amzn-Bedrock-Inference-Profile-ARN` header for you when making requests to Bedrock. ### Amazon SageMaker - **Model:** User-configurable (any model deployed in your SageMaker endpoint) ### Google GenAI - **Model:** User-configurable (any model supported by Google GenAI, e.g., `gemini-pro`) ### Huggingface - **Model:** User-configurable (any model supported by Huggingface Inference API) ### Google VertexAI - **Supported Models:** - gemini-1.0-pro-001 ### OCI GenAI - **Model:** User-configurable (any model supported by OCI GenAI) ### Custom REST - **Model:** User-configurable (any model your custom REST endpoint supports) ### IBM Watsonx - **Supported Models:** - ibm/granite-13b-chat-v2 ### Groq - **Model:** User-configurable (any model supported by Groq, e.g., `llama-3.3-70b-versatile`, `mixtral-8x7b-32768`) --- For more details on configuring each provider and model, refer to the official K8sGPT documentation and the provider's own documentation. ================================================ FILE: charts/k8sgpt/Chart.yaml ================================================ apiVersion: v2 appVersion: v0.4.23 #x-release-please-version description: A Helm chart for K8SGPT name: k8sgpt type: application version: 1.0.0 ================================================ FILE: charts/k8sgpt/templates/_helpers.tpl ================================================ {{/* Expand the name of the chart. */}} {{- define "k8sgpt.name" -}} {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} {{- end }} {{/* Create a default fully qualified app name. We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). If release name contains chart name it will be used as a full name. */}} {{- define "k8sgpt.fullname" -}} {{- if .Values.fullnameOverride }} {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} {{- else }} {{- $name := default .Chart.Name .Values.nameOverride }} {{- if contains $name .Release.Name }} {{- .Release.Name | trunc 63 | trimSuffix "-" }} {{- else }} {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} {{- end }} {{- end }} {{- end }} {{/* Create chart name and version as used by the chart label. */}} {{- define "k8sgpt.chart" -}} {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} {{- end }} {{/* Common labels */}} {{- define "k8sgpt.labels" -}} helm.sh/chart: {{ include "k8sgpt.chart" . }} app.kubernetes.io/name: {{ include "k8sgpt.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} app.kubernetes.io/managed-by: {{ .Release.Service }} {{- if .Chart.AppVersion }} app.kubernetes.io/version: {{ .Chart.AppVersion }} {{- end }} {{- end }} ================================================ FILE: charts/k8sgpt/templates/deployment.yaml ================================================ apiVersion: apps/v1 kind: Deployment metadata: name: {{ template "k8sgpt.fullname" . }} namespace: {{ .Release.Namespace | quote }} {{- if .Values.deployment.annotations }} annotations: {{- toYaml .Values.deployment.annotations | nindent 4 }} {{- end }} labels: {{- include "k8sgpt.labels" . | nindent 4 }} spec: replicas: 1 selector: matchLabels: app.kubernetes.io/name: {{ include "k8sgpt.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} template: metadata: labels: app.kubernetes.io/name: {{ include "k8sgpt.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} spec: {{- if .Values.deployment.securityContext }} securityContext: {{- toYaml .Values.deployment.securityContext | nindent 8 }} {{- end }} serviceAccountName: {{ template "k8sgpt.fullname" . }} containers: - name: k8sgpt-container imagePullPolicy: {{ .Values.deployment.imagePullPolicy }} image: {{ .Values.deployment.image.repository }}:{{ .Values.deployment.image.tag | default .Chart.AppVersion }} ports: - containerPort: 8080 {{- if .Values.deployment.mcp.enabled }} - containerPort: {{ .Values.deployment.mcp.port | int }} {{- end }} args: ["serve" {{- if .Values.deployment.mcp.enabled }}, "--mcp", "-v","--mcp-http", "--mcp-port", {{ .Values.deployment.mcp.port | quote }} {{- end }} ] {{- if .Values.deployment.resources }} resources: {{- toYaml .Values.deployment.resources | nindent 10 }} {{- end }} env: - name: K8SGPT_MODEL value: {{ .Values.deployment.env.model }} - name: K8SGPT_BACKEND value: {{ .Values.deployment.env.backend }} {{- if .Values.secret.secretKey }} - name: K8SGPT_PASSWORD valueFrom: secretKeyRef: name: ai-backend-secret key: secret-key {{- end }} - name: XDG_CONFIG_HOME value: /k8sgpt-config/ - name: XDG_CACHE_HOME value: /k8sgpt-config/ volumeMounts: - mountPath: /k8sgpt-config name: config volumes: - emptyDir: {} name: config ================================================ FILE: charts/k8sgpt/templates/role.yaml ================================================ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: {{ template "k8sgpt.fullname" . }} namespace: {{ .Release.Namespace | quote }} labels: {{- include "k8sgpt.labels" . | nindent 4 }} rules: - apiGroups: - '*' resources: - '*' verbs: - get - list - watch ================================================ FILE: charts/k8sgpt/templates/rolebinding.yaml ================================================ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: {{ template "k8sgpt.fullname" . }} namespace: {{ .Release.Namespace | quote }} labels: {{- include "k8sgpt.labels" . | nindent 4 }} subjects: - kind: ServiceAccount name: {{ template "k8sgpt.fullname" . }} namespace: {{ .Release.Namespace | quote }} roleRef: kind: ClusterRole name: {{ template "k8sgpt.fullname" . }} apiGroup: rbac.authorization.k8s.io ================================================ FILE: charts/k8sgpt/templates/sa.yaml ================================================ apiVersion: v1 kind: ServiceAccount metadata: name: {{ template "k8sgpt.fullname" . }} namespace: {{ .Release.Namespace | quote }} labels: {{- include "k8sgpt.labels" . | nindent 4 }} ================================================ FILE: charts/k8sgpt/templates/secret.yaml ================================================ {{- if .Values.secret.secretKey }} apiVersion: v1 data: secret-key: {{ .Values.secret.secretKey }} kind: Secret metadata: name: ai-backend-secret namespace: {{ .Release.Namespace | quote }} type: Opaque {{- end}} ================================================ FILE: charts/k8sgpt/templates/service.yaml ================================================ apiVersion: v1 kind: Service metadata: name: {{ template "k8sgpt.fullname" . }} namespace: {{ .Release.Namespace | quote }} labels: {{- include "k8sgpt.labels" . | nindent 4 }} {{- if .Values.service.annotations }} annotations: {{- toYaml .Values.service.annotations | nindent 4 }} {{- end }} spec: selector: app.kubernetes.io/name: {{ include "k8sgpt.name" . }} ports: - name: http port: 8080 targetPort: 8080 - name: metrics port: 8081 targetPort: 8081 {{- if .Values.deployment.mcp.enabled }} - name: mcp port: {{ .Values.deployment.mcp.port | int }} targetPort: {{ .Values.deployment.mcp.port | int }} {{- end }} type: {{ .Values.service.type }} ================================================ FILE: charts/k8sgpt/templates/serviceMonitor.yaml ================================================ {{- if .Values.serviceMonitor.enabled }} apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: name: {{ template "k8sgpt.fullname" . }} namespace: {{ .Release.Namespace | quote }} labels: {{- include "k8sgpt.labels" . | nindent 4 }} {{- if .Values.serviceMonitor.additionalLabels }} {{- toYaml .Values.serviceMonitor.additionalLabels | nindent 4 }} {{- end }} spec: endpoints: - honorLabels: true path: /metrics port: metrics selector: matchLabels: app.kubernetes.io/name: {{ include "k8sgpt.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end }} ================================================ FILE: charts/k8sgpt/values-mcp-example.yaml ================================================ # Example values file to enable MCP (Model Context Protocol) service # Copy this file and modify as needed, then use: helm install -f values-mcp-example.yaml deployment: # Enable MCP server mcp: enabled: true port: "8089" # Port for MCP server (default: 8089) http: true # Enable HTTP mode for MCP server # Other deployment settings remain the same image: repository: ghcr.io/k8sgpt-ai/k8sgpt tag: "" # defaults to Chart.appVersion if unspecified imagePullPolicy: Always env: model: "gpt-3.5-turbo" backend: "openai" resources: limits: cpu: "1" memory: "512Mi" requests: cpu: "0.2" memory: "156Mi" # Service configuration service: type: ClusterIP annotations: {} # Secret configuration for AI backend secret: secretKey: "" # base64 encoded OpenAI token # ServiceMonitor for Prometheus metrics serviceMonitor: enabled: false additionalLabels: {} ================================================ FILE: charts/k8sgpt/values.yaml ================================================ deployment: image: repository: ghcr.io/k8sgpt-ai/k8sgpt tag: "" # defaults to Chart.appVersion if unspecified imagePullPolicy: Always annotations: {} env: model: "gpt-3.5-turbo" backend: "openai" # one of: [ openai | llama ] # MCP (Model Context Protocol) server configuration mcp: enabled: false # Enable MCP server port: "8089" # Port for MCP server http: true # Enable HTTP mode for MCP server resources: limits: cpu: "1" memory: "512Mi" requests: cpu: "0.2" memory: "156Mi" securityContext: {} # Set securityContext.runAsUser/runAsGroup if necessary. Values below were taken from https://github.com/k8sgpt-ai/k8sgpt/blob/main/container/Dockerfile # runAsUser: 65532 # runAsGroup: 65532 secret: secretKey: "" # base64 encoded OpenAI token service: type: ClusterIP annotations: {} serviceMonitor: enabled: false additionalLabels: {} ================================================ FILE: cmd/analyze/analyze.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package analyze import ( "fmt" "os" "os/signal" "syscall" "github.com/fatih/color" "github.com/k8sgpt-ai/k8sgpt/pkg/ai/interactive" "github.com/k8sgpt-ai/k8sgpt/pkg/analysis" "github.com/spf13/cobra" "github.com/spf13/viper" ) var ( explain bool backend string output string filters []string language string nocache bool namespace string labelSelector string anonymize bool maxConcurrency int withDoc bool interactiveMode bool customAnalysis bool customHeaders []string withStats bool ) // AnalyzeCmd represents the problems command var AnalyzeCmd = &cobra.Command{ Use: "analyze", Aliases: []string{"analyse"}, Short: "This command will find problems within your Kubernetes cluster", Long: `This command will find problems within your Kubernetes cluster and provide you with a list of issues that need to be resolved`, Run: func(cmd *cobra.Command, args []string) { // Create analysis configuration first. config, err := analysis.NewAnalysis( backend, language, filters, namespace, labelSelector, nocache, explain, maxConcurrency, withDoc, interactiveMode, customHeaders, withStats, ) verbose := viper.GetBool("verbose") if verbose { fmt.Println("Debug: Checking analysis configuration.") } if err != nil { color.Red("Error: %v", err) os.Exit(1) } if verbose { fmt.Println("Debug: Analysis initialized.") } defer config.Close() if customAnalysis { config.RunCustomAnalysis() if verbose { fmt.Println("Debug: All custom analyzers completed.") } } config.RunAnalysis() if verbose { fmt.Println("Debug: All core analyzers completed.") } if explain { err := config.GetAIResults(output, anonymize) if verbose { fmt.Println("Debug: Checking AI results.") } if err != nil { color.Red("Error: %v", err) os.Exit(1) } } // print results output_data, err := config.PrintOutput(output) if verbose { fmt.Println("Debug: Checking output.") } if err != nil { color.Red("Error: %v", err) os.Exit(1) } if withStats { statsData := config.PrintStats() fmt.Println(string(statsData)) } fmt.Println(string(output_data)) if interactiveMode && explain { if output == "json" { color.Yellow("Caution: interactive mode using --json enabled may use additional tokens.") } sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) interactiveClient := interactive.NewInteractionRunner(config, output_data) go interactiveClient.StartInteraction() for { select { case res := <-sigs: switch res { default: os.Exit(0) } case res := <-interactiveClient.State: switch res { case interactive.E_EXITED: os.Exit(0) } } } } }, } func init() { // namespace flag AnalyzeCmd.Flags().StringVarP(&namespace, "namespace", "n", "", "Namespace to analyze") // no cache flag AnalyzeCmd.Flags().BoolVarP(&nocache, "no-cache", "c", false, "Do not use cached data") // anonymize flag AnalyzeCmd.Flags().BoolVarP(&anonymize, "anonymize", "a", false, "Anonymize data before sending it to the AI backend. This flag masks sensitive data, such as Kubernetes object names and labels, by replacing it with a key. However, please note that this flag does not currently apply to events.") // array of strings flag AnalyzeCmd.Flags().StringSliceVarP(&filters, "filter", "f", []string{}, "Filter for these analyzers (e.g. Pod, PersistentVolumeClaim, Service, ReplicaSet)") // explain flag AnalyzeCmd.Flags().BoolVarP(&explain, "explain", "e", false, "Explain the problem to me") // add flag for backend AnalyzeCmd.Flags().StringVarP(&backend, "backend", "b", "", "Backend AI provider") // output as json AnalyzeCmd.Flags().StringVarP(&output, "output", "o", "text", "Output format (text, json)") // add language options for output AnalyzeCmd.Flags().StringVarP(&language, "language", "l", "english", "Languages to use for AI (e.g. 'English', 'Spanish', 'French', 'German', 'Italian', 'Portuguese', 'Dutch', 'Russian', 'Chinese', 'Japanese', 'Korean')") // add max concurrency AnalyzeCmd.Flags().IntVarP(&maxConcurrency, "max-concurrency", "m", 10, "Maximum number of concurrent requests to the Kubernetes API server") // kubernetes doc flag AnalyzeCmd.Flags().BoolVarP(&withDoc, "with-doc", "d", false, "Give me the official documentation of the involved field") // interactive mode flag AnalyzeCmd.Flags().BoolVarP(&interactiveMode, "interactive", "i", false, "Enable interactive mode that allows further conversation with LLM about the problem. Works only with --explain flag") // custom analysis flag AnalyzeCmd.Flags().BoolVarP(&customAnalysis, "custom-analysis", "z", false, "Enable custom analyzers") // add custom headers flag AnalyzeCmd.Flags().StringSliceVarP(&customHeaders, "custom-headers", "r", []string{}, "Custom Headers, : (e.g CustomHeaderKey:CustomHeaderValue AnotherHeader:AnotherValue)") // label selector flag AnalyzeCmd.Flags().StringVarP(&labelSelector, "selector", "L", "", "Label selector (label query) to filter on, supports '=', '==', and '!='. (e.g. -L key1=value1,key2=value2). Matching objects must satisfy all of the specified label constraints.") // print stats AnalyzeCmd.Flags().BoolVarP(&withStats, "with-stat", "s", false, "Print analysis stats. This option disables errors display.") } ================================================ FILE: cmd/auth/add.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package auth import ( "fmt" "os" "strings" "syscall" "github.com/fatih/color" "github.com/k8sgpt-ai/k8sgpt/pkg/ai" "github.com/spf13/cobra" "github.com/spf13/viper" "golang.org/x/term" ) const ( defaultBackend = "openai" defaultModel = "gpt-4o" ) var addCmd = &cobra.Command{ Use: "add", Short: "Add new provider", Long: "The add command allows to configure a new backend AI provider", PreRun: func(cmd *cobra.Command, args []string) { backend, _ := cmd.Flags().GetString("backend") if strings.ToLower(backend) == "azureopenai" { _ = cmd.MarkFlagRequired("engine") _ = cmd.MarkFlagRequired("baseurl") } if strings.ToLower(backend) == "amazonsagemaker" { _ = cmd.MarkFlagRequired("endpointname") _ = cmd.MarkFlagRequired("providerRegion") } if strings.ToLower(backend) == "amazonbedrock" { _ = cmd.MarkFlagRequired("providerRegion") } if strings.ToLower(backend) == "ibmwatsonxai" { _ = cmd.MarkFlagRequired("providerId") } }, Run: func(cmd *cobra.Command, args []string) { validBackend := func(validBackends []string, backend string) bool { for _, b := range validBackends { if b == backend { return true } } return false } // check if backend is not empty and a valid value if backend == "" { color.Yellow(fmt.Sprintf("Warning: backend input is empty, will use the default value: %s", defaultBackend)) backend = defaultBackend } else { if !validBackend(ai.Backends, backend) { color.Red("Error: Backend AI accepted values are '%v'", strings.Join(ai.Backends, ", ")) os.Exit(1) } } // get ai configuration err := viper.UnmarshalKey("ai", &configAI) if err != nil { color.Red("Error: %v", err) os.Exit(1) } // search for provider with same name providerIndex := -1 for i, provider := range configAI.Providers { if backend == provider.Name { providerIndex = i break } } if providerIndex != -1 { // provider with same name exists, update provider info color.Yellow("Provider with same name already exists.") os.Exit(1) } // check if model is not empty if model == "" { model = defaultModel color.Yellow(fmt.Sprintf("Warning: model input is empty, will use the default value: %s", defaultModel)) } if temperature > 1.0 || temperature < 0.0 { color.Red("Error: temperature ranges from 0 to 1.") os.Exit(1) } if topP > 1.0 || topP < 0.0 { color.Red("Error: topP ranges from 0 to 1.") os.Exit(1) } if topK < 1 || topK > 100 { color.Red("Error: topK ranges from 1 to 100.") os.Exit(1) } if ai.NeedPassword(backend) && password == "" { fmt.Printf("Enter %s Key: ", backend) bytePassword, err := term.ReadPassword(int(syscall.Stdin)) if err != nil { color.Red("Error reading %s Key from stdin: %s", backend, err.Error()) os.Exit(1) } password = strings.TrimSpace(string(bytePassword)) } // create new provider object newProvider := ai.AIProvider{ Name: backend, Model: model, Password: password, BaseURL: baseURL, EndpointName: endpointName, Engine: engine, Temperature: temperature, ProviderRegion: providerRegion, ProviderId: providerId, CompartmentId: compartmentId, TopP: topP, TopK: topK, MaxTokens: maxTokens, OrganizationId: organizationId, } if providerIndex == -1 { // provider with same name does not exist, add new provider to list configAI.Providers = append(configAI.Providers, newProvider) viper.Set("ai", configAI) if err := viper.WriteConfig(); err != nil { color.Red("Error writing config file: %s", err.Error()) os.Exit(1) } color.Green("%s added to the AI backend provider list", backend) } }, } func init() { // add flag for backend addCmd.Flags().StringVarP(&backend, "backend", "b", defaultBackend, "Backend AI provider") // add flag for model addCmd.Flags().StringVarP(&model, "model", "m", defaultModel, "Backend AI model") // add flag for password addCmd.Flags().StringVarP(&password, "password", "p", "", "Backend AI password") // add flag for url addCmd.Flags().StringVarP(&baseURL, "baseurl", "u", "", "URL AI provider, (e.g `http://localhost:8080/v1`)") // add flag for endpointName addCmd.Flags().StringVarP(&endpointName, "endpointname", "n", "", "Endpoint Name, e.g. `endpoint-xxxxxxxxxxxx` (only for amazonbedrock, amazonsagemaker backends)") // add flag for topP addCmd.Flags().Float32VarP(&topP, "topp", "", 0.5, "Probability Cutoff: Set a threshold (0.0-1.0) to limit word choices. Higher values add randomness, lower values increase predictability.") // add flag for topK addCmd.Flags().Int32VarP(&topK, "topk", "c", 50, "Sampling Cutoff: Set a threshold (1-100) to restrict the sampling process to the top K most probable words at each step. Higher values lead to greater variability, lower values increases predictability.") // max tokens addCmd.Flags().IntVarP(&maxTokens, "maxtokens", "l", 2048, "Specify a maximum output length. Adjust (1-...) to control text length. Higher values produce longer output, lower values limit length") // add flag for temperature addCmd.Flags().Float32VarP(&temperature, "temperature", "t", 0.7, "The sampling temperature, value ranges between 0 ( output be more deterministic) and 1 (more random)") // add flag for azure open ai engine/deployment name addCmd.Flags().StringVarP(&engine, "engine", "e", "", "Azure AI deployment name (only for azureopenai backend)") //add flag for amazonbedrock region name addCmd.Flags().StringVarP(&providerRegion, "providerRegion", "r", "", "Provider Region name (only for amazonbedrock, googlevertexai backend)") //add flag for vertexAI/WatsonxAI Project ID addCmd.Flags().StringVarP(&providerId, "providerId", "i", "", "Provider specific ID for e.g. project (only for googlevertexai/ibmwatsonxai backend)") //add flag for OCI Compartment ID addCmd.Flags().StringVarP(&compartmentId, "compartmentId", "k", "", "Compartment ID for generative AI model (only for oci backend)") // add flag for openai organization addCmd.Flags().StringVarP(&organizationId, "organizationId", "o", "", "OpenAI or AzureOpenAI Organization ID (only for openai and azureopenai backend)") } ================================================ FILE: cmd/auth/auth.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package auth import ( "github.com/k8sgpt-ai/k8sgpt/pkg/ai" "github.com/spf13/cobra" ) var ( backend string password string baseURL string endpointName string model string engine string temperature float32 providerRegion string providerId string compartmentId string topP float32 topK int32 maxTokens int organizationId string ) var configAI ai.AIConfiguration // authCmd represents the auth command var AuthCmd = &cobra.Command{ Use: "auth", Short: "Authenticate with your chosen backend", Long: `Provide the necessary credentials to authenticate with your chosen backend.`, Run: func(cmd *cobra.Command, args []string) { if len(args) == 0 { _ = cmd.Help() return } }, } func init() { // add subcommand to list backends AuthCmd.AddCommand(listCmd) // add subcommand to create new backend provider AuthCmd.AddCommand(addCmd) // add subcommand to remove new backend provider AuthCmd.AddCommand(removeCmd) // add subcommand to set default backend provider AuthCmd.AddCommand(defaultCmd) // add subcommand to update backend provider AuthCmd.AddCommand(updateCmd) } ================================================ FILE: cmd/auth/default.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package auth import ( "os" "strings" "github.com/fatih/color" "github.com/spf13/cobra" "github.com/spf13/viper" ) var ( providerName string ) var defaultCmd = &cobra.Command{ Use: "default", Short: "Set your default AI backend provider", Long: "The command to set your new default AI backend provider (default is openai)", Run: func(cmd *cobra.Command, args []string) { err := viper.UnmarshalKey("ai", &configAI) if err != nil { color.Red("Error: %v", err) os.Exit(1) } if providerName == "" { if configAI.DefaultProvider != "" { color.Yellow("Your default provider is %s", configAI.DefaultProvider) } else { color.Yellow("Your default provider is openai") } os.Exit(0) } // lowercase the provider name providerName = strings.ToLower(providerName) // Check if the provider is in the provider list providerExists := false for _, provider := range configAI.Providers { if provider.Name == providerName { providerExists = true } } if !providerExists { color.Red("Error: Provider %s does not exist", providerName) os.Exit(1) } // Set the default provider configAI.DefaultProvider = providerName viper.Set("ai", configAI) // Viper write config err = viper.WriteConfig() if err != nil { color.Red("Error: %v", err) os.Exit(1) } // Print acknowledgement color.Green("Default provider set to %s", providerName) }, } func init() { // provider name flag defaultCmd.Flags().StringVarP(&providerName, "provider", "p", "", "The name of the provider to set as default") } ================================================ FILE: cmd/auth/list.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package auth import ( "fmt" "os" "github.com/fatih/color" "github.com/k8sgpt-ai/k8sgpt/pkg/ai" "github.com/spf13/cobra" "github.com/spf13/viper" ) var details bool var listCmd = &cobra.Command{ Use: "list", Short: "List configured providers", Long: "The list command displays a list of configured providers", Run: func(cmd *cobra.Command, args []string) { // get ai configuration err := viper.UnmarshalKey("ai", &configAI) if err != nil { color.Red("Error: %v", err) os.Exit(1) } // Print the default if it is set fmt.Print(color.YellowString("Default: \n")) if configAI.DefaultProvider != "" { fmt.Printf("> %s\n", color.BlueString(configAI.DefaultProvider)) } else { fmt.Printf("> %s\n", color.BlueString("openai")) } // Get list of all AI Backends and only print them if they are not in the provider list fmt.Print(color.YellowString("Active: \n")) for _, aiBackend := range ai.Backends { providerExists := false for _, provider := range configAI.Providers { if provider.Name == aiBackend { providerExists = true } } if providerExists { fmt.Printf("> %s\n", color.GreenString(aiBackend)) if details { for _, provider := range configAI.Providers { if provider.Name == aiBackend { printDetails(provider) } } } } } fmt.Print(color.YellowString("Unused: \n")) for _, aiBackend := range ai.Backends { providerExists := false for _, provider := range configAI.Providers { if provider.Name == aiBackend { providerExists = true } } if !providerExists { fmt.Printf("> %s\n", color.RedString(aiBackend)) } } }, } func init() { listCmd.Flags().BoolVar(&details, "details", false, "Print active provider configuration details") } func printDetails(provider ai.AIProvider) { if provider.Model != "" { fmt.Printf(" - Model: %s\n", provider.Model) } if provider.Engine != "" { fmt.Printf(" - Engine: %s\n", provider.Engine) } if provider.BaseURL != "" { fmt.Printf(" - BaseURL: %s\n", provider.BaseURL) } } ================================================ FILE: cmd/auth/remove.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package auth import ( "os" "strings" "github.com/fatih/color" "github.com/spf13/cobra" "github.com/spf13/viper" ) var removeCmd = &cobra.Command{ Use: "remove", Short: "Remove provider(s)", Long: "The command to remove AI backend provider(s)", PreRun: func(cmd *cobra.Command, args []string) { _ = cmd.MarkFlagRequired("backends") }, Run: func(cmd *cobra.Command, args []string) { if backend == "" { color.Red("Error: backends must be set.") _ = cmd.Help() return } inputBackends := strings.Split(backend, ",") err := viper.UnmarshalKey("ai", &configAI) if err != nil { color.Red("Error: %v", err) os.Exit(1) } for _, b := range inputBackends { foundBackend := false for i, provider := range configAI.Providers { if b == provider.Name { foundBackend = true configAI.Providers = append(configAI.Providers[:i], configAI.Providers[i+1:]...) if configAI.DefaultProvider == b { configAI.DefaultProvider = "openai" } color.Green("%s deleted from the AI backend provider list", b) break } } if !foundBackend { color.Red("Error: %s does not exist in configuration file. Please use k8sgpt auth new.", b) os.Exit(1) } } viper.Set("ai", configAI) if err := viper.WriteConfig(); err != nil { color.Red("Error writing config file: %s", err.Error()) os.Exit(1) } }, } func init() { // add flag for backends removeCmd.Flags().StringVarP(&backend, "backends", "b", "", "Backend AI providers to remove (separated by a comma)") } ================================================ FILE: cmd/auth/update.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package auth import ( "os" "strings" "github.com/fatih/color" "github.com/spf13/cobra" "github.com/spf13/viper" ) var updateCmd = &cobra.Command{ Use: "update", Short: "Update a backend provider", Long: "The command to update an AI backend provider", // Args: cobra.ExactArgs(1), PreRun: func(cmd *cobra.Command, args []string) { _ = cmd.MarkFlagRequired("backend") backend, _ := cmd.Flags().GetString("backend") if strings.ToLower(backend) == "azureopenai" { _ = cmd.MarkFlagRequired("engine") _ = cmd.MarkFlagRequired("baseurl") } organizationId, _ := cmd.Flags().GetString("organizationId") if strings.ToLower(backend) != "azureopenai" && strings.ToLower(backend) != "openai" { if organizationId != "" { color.Red("Error: organizationId must be empty for backends other than azureopenai or openai.") os.Exit(1) } } }, Run: func(cmd *cobra.Command, args []string) { // get ai configuration err := viper.UnmarshalKey("ai", &configAI) if err != nil { color.Red("Error: %v", err) os.Exit(1) } backend, _ := cmd.Flags().GetString("backend") if temperature > 1.0 || temperature < 0.0 { color.Red("Error: temperature ranges from 0 to 1.") os.Exit(1) } foundBackend := false for i, provider := range configAI.Providers { if backend == provider.Name { foundBackend = true if backend != "" { configAI.Providers[i].Name = backend color.Blue("Backend name updated successfully") } if model != "" { configAI.Providers[i].Model = model color.Blue("Model updated successfully") } if password != "" { configAI.Providers[i].Password = password color.Blue("Password updated successfully") } if baseURL != "" { configAI.Providers[i].BaseURL = baseURL color.Blue("Base URL updated successfully") } if engine != "" { configAI.Providers[i].Engine = engine } if organizationId != "" { configAI.Providers[i].OrganizationId = organizationId color.Blue("Organization Id updated successfully") } configAI.Providers[i].Temperature = temperature color.Green("%s updated in the AI backend provider list", backend) } } if !foundBackend { color.Red("Error: %s does not exist in configuration file. Please use k8sgpt auth new.", backend) os.Exit(1) } viper.Set("ai", configAI) if err := viper.WriteConfig(); err != nil { color.Red("Error writing config file: %s", err.Error()) os.Exit(1) } }, } func init() { // update flag for backend updateCmd.Flags().StringVarP(&backend, "backend", "b", "", "Update backend AI provider") // update flag for model updateCmd.Flags().StringVarP(&model, "model", "m", "", "Update backend AI model") // update flag for password updateCmd.Flags().StringVarP(&password, "password", "p", "", "Update backend AI password") // update flag for url updateCmd.Flags().StringVarP(&baseURL, "baseurl", "u", "", "Update URL AI provider, (e.g `http://localhost:8080/v1`)") // add flag for temperature updateCmd.Flags().Float32VarP(&temperature, "temperature", "t", 0.7, "The sampling temperature, value ranges between 0 ( output be more deterministic) and 1 (more random)") // update flag for azure open ai engine/deployment name updateCmd.Flags().StringVarP(&engine, "engine", "e", "", "Update Azure AI deployment name") // update flag for organizationId updateCmd.Flags().StringVarP(&organizationId, "organizationId", "o", "", "Update OpenAI or Azure organization Id") } ================================================ FILE: cmd/cache/add.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cache import ( "fmt" "os" "strings" "github.com/fatih/color" "github.com/k8sgpt-ai/k8sgpt/pkg/cache" "github.com/spf13/cobra" ) var ( region string //nolint:unused bucketName string storageAccount string containerName string projectId string endpoint string insecure bool ) // addCmd represents the add command var addCmd = &cobra.Command{ Use: "add [cache type]", Short: "Add a remote cache", Long: `This command allows you to add a remote cache to store the results of an analysis. The supported cache types are: - Azure Blob storage (e.g., k8sgpt cache add azure) - Google Cloud storage (e.g., k8sgpt cache add gcs) - S3 (e.g., k8sgpt cache add s3) - Interplex (e.g., k8sgpt cache add interplex)`, Run: func(cmd *cobra.Command, args []string) { if len(args) == 0 { color.Red("Error: Please provide a value for cache types. Run k8sgpt cache add --help") os.Exit(1) } fmt.Println(color.YellowString("Adding remote based cache")) cacheType := args[0] remoteCache, err := cache.NewCacheProvider(strings.ToLower(cacheType), bucketName, region, endpoint, storageAccount, containerName, projectId, insecure) if err != nil { color.Red("Error: %v", err) os.Exit(1) } err = cache.AddRemoteCache(remoteCache) if err != nil { color.Red("Error: %v", err) os.Exit(1) } }, } func init() { CacheCmd.AddCommand(addCmd) addCmd.Flags().StringVarP(®ion, "region", "r", "us-east-1", "The region to use for the AWS S3 or GCS cache") addCmd.Flags().StringVarP(&endpoint, "endpoint", "e", "", "The S3 or minio endpoint") addCmd.Flags().BoolVarP(&insecure, "insecure", "i", false, "Skip TLS verification for S3/Minio custom endpoint") addCmd.Flags().StringVarP(&bucketName, "bucket", "b", "", "The name of the AWS S3 bucket to use for the cache") addCmd.Flags().StringVarP(&projectId, "projectid", "p", "", "The GCP project ID") addCmd.Flags().StringVarP(&storageAccount, "storageacc", "s", "", "The Azure storage account name of the container") addCmd.Flags().StringVarP(&containerName, "container", "c", "", "The Azure container name to use for the cache") addCmd.MarkFlagsRequiredTogether("storageacc", "container") // Tedious check to ensure we don't include arguments from different providers addCmd.MarkFlagsMutuallyExclusive("region", "storageacc") addCmd.MarkFlagsMutuallyExclusive("region", "container") addCmd.MarkFlagsMutuallyExclusive("bucket", "storageacc") addCmd.MarkFlagsMutuallyExclusive("bucket", "container") addCmd.MarkFlagsMutuallyExclusive("projectid", "storageacc") addCmd.MarkFlagsMutuallyExclusive("projectid", "container") } ================================================ FILE: cmd/cache/cache.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cache import ( "github.com/spf13/cobra" ) // cacheCmd represents the cache command var CacheCmd = &cobra.Command{ Use: "cache", Short: "For working with the cache the results of an analysis", Long: `Cache commands allow you to add a remote cache, list the contents of the cache, and remove items from the cache.`, Run: func(cmd *cobra.Command, args []string) { err := cmd.Help() if err != nil { panic(err) } }, } func init() { } ================================================ FILE: cmd/cache/get.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cache import ( "fmt" "github.com/fatih/color" "github.com/k8sgpt-ai/k8sgpt/pkg/cache" "github.com/spf13/cobra" "os" ) // listCmd represents the list command var getCmd = &cobra.Command{ Use: "get", Short: "Get the current cache", Long: `Returns the current remote cache being used`, Run: func(cmd *cobra.Command, args []string) { // load remote cache if it is configured c, err := cache.GetCacheConfiguration() if err != nil { color.Red("Error: %v", err) os.Exit(1) } fmt.Printf("Current remote cache is: %s", c.GetName()) }, } func init() { CacheCmd.AddCommand(getCmd) } ================================================ FILE: cmd/cache/list.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cache import ( "os" "reflect" "github.com/fatih/color" "github.com/k8sgpt-ai/k8sgpt/pkg/cache" "github.com/olekukonko/tablewriter" "github.com/spf13/cobra" ) // listCmd represents the list command var listCmd = &cobra.Command{ Use: "list", Short: "List the contents of the cache", Long: `This command allows you to list the contents of the cache.`, Run: func(cmd *cobra.Command, args []string) { // load remote cache if it is configured c, err := cache.GetCacheConfiguration() if err != nil { color.Red("Error: %v", err) os.Exit(1) } names, err := c.List() if err != nil { color.Red("Error: %v", err) os.Exit(1) } var headers []string obj := cache.CacheObjectDetails{} objType := reflect.TypeOf(obj) for i := 0; i < objType.NumField(); i++ { field := objType.Field(i) headers = append(headers, field.Name) } table := tablewriter.NewWriter(os.Stdout) table.SetHeader(headers) for _, v := range names { table.Append([]string{v.Name, v.UpdatedAt.String()}) } table.Render() }, } func init() { CacheCmd.AddCommand(listCmd) } ================================================ FILE: cmd/cache/purge.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cache import ( "fmt" "os" "github.com/fatih/color" "github.com/k8sgpt-ai/k8sgpt/pkg/cache" "github.com/spf13/cobra" ) var all bool var purgeCmd = &cobra.Command{ Use: "purge [object name]", Short: "Purge a remote cache", Long: "This command allows you to delete/purge one object from the cache or all objects with --all flag.", Run: func(cmd *cobra.Command, args []string) { c, err := cache.GetCacheConfiguration() if err != nil { color.Red("Error: %v", err) os.Exit(1) } if all { fmt.Println(color.YellowString("Purging all objects from the remote cache.")) names, err := c.List() if err != nil { color.Red("Error listing cache objects: %v", err) os.Exit(1) } if len(names) == 0 { fmt.Println(color.GreenString("No objects to delete.")) return } var failed []string for _, obj := range names { err := c.Remove(obj.Name) if err != nil { failed = append(failed, obj.Name) } } if len(failed) > 0 { color.Red("Failed to delete: %v", failed) os.Exit(1) } fmt.Println(color.GreenString("All objects deleted.")) return } if len(args) == 0 { color.Red("Error: Please provide a value for object name or use --all. Run k8sgpt cache purge --help") os.Exit(1) } objectKey := args[0] fmt.Println(color.YellowString("Purging a remote cache.")) err = c.Remove(objectKey) if err != nil { color.Red("Error: %v", err) os.Exit(1) } fmt.Println(color.GreenString("Object deleted.")) }, } func init() { purgeCmd.Flags().BoolVar(&all, "all", false, "Purge all objects in the cache") CacheCmd.AddCommand(purgeCmd) } ================================================ FILE: cmd/cache/remove.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cache import ( "os" "github.com/fatih/color" "github.com/k8sgpt-ai/k8sgpt/pkg/cache" "github.com/spf13/cobra" ) // removeCmd represents the remove command var removeCmd = &cobra.Command{ Use: "remove", Short: "Remove the remote cache", Long: `This command allows you to remove the remote cache and use the default filecache.`, Run: func(cmd *cobra.Command, args []string) { err := cache.RemoveRemoteCache() if err != nil { color.Red("Error: %v", err) os.Exit(1) } color.Green("Successfully removed the remote cache") }, } func init() { CacheCmd.AddCommand(removeCmd) } ================================================ FILE: cmd/customAnalyzer/add.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package customanalyzer import ( "os" "github.com/fatih/color" customAnalyzer "github.com/k8sgpt-ai/k8sgpt/pkg/custom_analyzer" "github.com/spf13/cobra" "github.com/spf13/viper" ) var ( name string url string port int ) var addCmd = &cobra.Command{ Use: "add", Aliases: []string{"add"}, Short: "This command will add a custom analyzer from source", Long: "This command allows you to add/remote/list an existing custom analyzer.", Run: func(cmd *cobra.Command, args []string) { err := viper.UnmarshalKey("custom_analyzers", &configCustomAnalyzer) if err != nil { color.Red("Error: %v", err) os.Exit(1) } analyzer := customAnalyzer.NewCustomAnalyzer() // Check if configuration is valid err = analyzer.Check(configCustomAnalyzer, name, url, port) if err != nil { color.Red("Error adding custom analyzer: %s", err.Error()) os.Exit(1) } configCustomAnalyzer = append(configCustomAnalyzer, customAnalyzer.CustomAnalyzerConfiguration{ Name: name, Connection: customAnalyzer.Connection{ Url: url, Port: port, }, }) viper.Set("custom_analyzers", configCustomAnalyzer) if err := viper.WriteConfig(); err != nil { color.Red("Error writing config file: %s", err.Error()) os.Exit(1) } color.Green("%s added to the custom analyzers config list", name) }, } func init() { addCmd.Flags().StringVarP(&name, "name", "n", "my-custom-analyzer", "Name of the custom analyzer.") addCmd.Flags().StringVarP(&url, "url", "u", "localhost", "URL for the custom analyzer connection.") addCmd.Flags().IntVarP(&port, "port", "r", 8085, "Port for the custom analyzer connection.") } ================================================ FILE: cmd/customAnalyzer/customAnalyzer.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package customanalyzer import ( customAnalyzer "github.com/k8sgpt-ai/k8sgpt/pkg/custom_analyzer" "github.com/spf13/cobra" ) var configCustomAnalyzer []customAnalyzer.CustomAnalyzerConfiguration // authCmd represents the auth command var CustomAnalyzerCmd = &cobra.Command{ Use: "custom-analyzer", Short: "Manage a custom analyzer", Long: `This command allows you to manage custom analyzers, including adding, removing, and listing them.`, Run: func(cmd *cobra.Command, args []string) { if len(args) == 0 { _ = cmd.Help() return } }, } func init() { // add subcommand to add custom analyzer CustomAnalyzerCmd.AddCommand(addCmd) // remove subcomment to remove custom analyzer CustomAnalyzerCmd.AddCommand(removeCmd) // list subcomment to list custom analyzer CustomAnalyzerCmd.AddCommand(listCmd) } ================================================ FILE: cmd/customAnalyzer/list.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package customanalyzer import ( "fmt" "os" "github.com/fatih/color" customAnalyzer "github.com/k8sgpt-ai/k8sgpt/pkg/custom_analyzer" "github.com/spf13/cobra" "github.com/spf13/viper" ) var details bool var listCmd = &cobra.Command{ Use: "list", Short: "List configured custom analyzers", Long: "The list command displays a list of configured custom analyzers", Run: func(cmd *cobra.Command, args []string) { // get custom_analyzers configuration err := viper.UnmarshalKey("custom_analyzers", &configCustomAnalyzer) if err != nil { color.Red("Error: %v", err) os.Exit(1) } // Get list of all Custom Analyers configured fmt.Print(color.YellowString("Active: \n")) for _, analyzer := range configCustomAnalyzer { fmt.Printf("> %s\n", color.GreenString(analyzer.Name)) if details { printDetails(analyzer) } } }, } func init() { listCmd.Flags().BoolVar(&details, "details", false, "Print custom analyzers configuration details") } func printDetails(analyzer customAnalyzer.CustomAnalyzerConfiguration) { fmt.Printf(" - Url: %s\n", analyzer.Connection.Url) fmt.Printf(" - Port: %d\n", analyzer.Connection.Port) } ================================================ FILE: cmd/customAnalyzer/remove.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package customanalyzer import ( "os" "strings" "github.com/fatih/color" "github.com/spf13/cobra" "github.com/spf13/viper" ) var ( names string ) var removeCmd = &cobra.Command{ Use: "remove", Short: "Remove custom analyzer(s)", Long: "The command to remove custom analyzer(s)", PreRun: func(cmd *cobra.Command, args []string) { // Ensure that the "names" flag is provided before running the command _ = cmd.MarkFlagRequired("names") }, Run: func(cmd *cobra.Command, args []string) { if names == "" { // Display an error message and show command help if "names" is not set color.Red("Error: names must be set.") _ = cmd.Help() return } // Split the provided names by comma inputCustomAnalyzers := strings.Split(names, ",") // Load the custom analyzers from the configuration file err := viper.UnmarshalKey("custom_analyzers", &configCustomAnalyzer) if err != nil { // Display an error message if the configuration cannot be loaded color.Red("Error: %v", err) os.Exit(1) } // Iterate over each input analyzer name for _, inputAnalyzer := range inputCustomAnalyzers { foundAnalyzer := false // Search for the analyzer in the current configuration for i, analyzer := range configCustomAnalyzer { if analyzer.Name == inputAnalyzer { foundAnalyzer = true // Remove the analyzer from the configuration list configCustomAnalyzer = append(configCustomAnalyzer[:i], configCustomAnalyzer[i+1:]...) color.Green("%s deleted from the custom analyzer list", analyzer.Name) break } } if !foundAnalyzer { // Display an error if the analyzer is not found in the configuration color.Red("Error: %s does not exist in configuration file. Please use k8sgpt custom-analyzer add.", inputAnalyzer) os.Exit(1) } } // Save the updated configuration back to the file viper.Set("custom_analyzers", configCustomAnalyzer) if err := viper.WriteConfig(); err != nil { // Display an error if the configuration cannot be written color.Red("Error writing config file: %s", err.Error()) os.Exit(1) } }, } func init() { // add flag for names removeCmd.Flags().StringVarP(&names, "names", "n", "", "Custom analyzers to remove (separated by a comma)") } ================================================ FILE: cmd/dump/dump.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package dump import ( "encoding/json" "fmt" "net/http" "os" "time" "github.com/fatih/color" "github.com/k8sgpt-ai/k8sgpt/pkg/ai" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" "github.com/spf13/cobra" "github.com/spf13/viper" "k8s.io/apimachinery/pkg/version" ) type K8sGPTInfo struct { Version string Commit string Date string } type DumpOut struct { AIConfiguration ai.AIConfiguration ActiveFilters []string KubenetesServerVersion *version.Info K8sGPTInfo K8sGPTInfo } var DumpCmd = &cobra.Command{ Use: "dump", Short: "Creates a dumpfile for debugging issues with K8sGPT", Long: `The dump command will create a dump.*.json which will contain K8sGPT non-sensitive configuration information.`, Run: func(cmd *cobra.Command, args []string) { // Fetch the configuration object(s) // get ai configuration var configAI ai.AIConfiguration err := viper.UnmarshalKey("ai", &configAI) if err != nil { color.Red("Error: %v", err) os.Exit(1) } var newProvider []ai.AIProvider for _, config := range configAI.Providers { // we blank out the custom headers for data protection reasons config.CustomHeaders = make([]http.Header, 0) // blank out the password if len(config.Password) > 4 { config.Password = config.Password[:4] + "***" } else { // If the password is shorter than 4 characters config.Password = "***" } newProvider = append(newProvider, config) } configAI.Providers = newProvider activeFilters := viper.GetStringSlice("active_filters") kubecontext := viper.GetString("kubecontext") kubeconfig := viper.GetString("kubeconfig") client, err := kubernetes.NewClient(kubecontext, kubeconfig) if err != nil { color.Red("Error: %v", err) os.Exit(1) } v, err := client.Client.Discovery().ServerVersion() if err != nil { color.Yellow("Could not find kubernetes server version") } var dumpOut DumpOut = DumpOut{ AIConfiguration: configAI, ActiveFilters: activeFilters, KubenetesServerVersion: v, K8sGPTInfo: K8sGPTInfo{ Version: viper.GetString("Version"), Commit: viper.GetString("Commit"), Date: viper.GetString("Date"), }, } // Serialize dumpOut to JSON jsonData, err := json.MarshalIndent(dumpOut, "", " ") if err != nil { color.Red("Error: %v", err) os.Exit(1) } // Write JSON data to file f := fmt.Sprintf("dump_%s.json", time.Now().Format("20060102150405")) err = os.WriteFile(f, jsonData, 0644) if err != nil { color.Red("Error: %v", err) os.Exit(1) } color.Green("Dump created successfully: %s", f) }, } func init() { } ================================================ FILE: cmd/filters/add.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package filters import ( "os" "strings" "github.com/fatih/color" "github.com/k8sgpt-ai/k8sgpt/pkg/analyzer" "github.com/k8sgpt-ai/k8sgpt/pkg/util" "github.com/spf13/cobra" "github.com/spf13/viper" ) var addCmd = &cobra.Command{ Use: "add [filter(s)]", Short: "Adds one or more new filters.", Long: `The add command adds one or more new filters to the default set of filters used by the analyze.`, Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { inputFilters := strings.Split(args[0], ",") coreFilters, additionalFilters, integrationFilters := analyzer.ListFilters() availableFilters := append(append(coreFilters, additionalFilters...), integrationFilters...) // Verify filter exist invalidFilters := []string{} for _, f := range inputFilters { if f == "" { color.Red("Filter cannot be empty. Please use correct syntax.") os.Exit(1) } foundFilter := false for _, filter := range availableFilters { if filter == f { foundFilter = true // WARNING: This is to enable users correctly understand implications // of enabling logs if filter == "Log" { color.Yellow("Warning: by enabling logs, you will be sending potentially sensitive data to the AI backend.") } break } } if !foundFilter { invalidFilters = append(invalidFilters, f) } } if len(invalidFilters) != 0 { color.Red("Filter %s does not exist. Please use k8sgpt filters list", strings.Join(invalidFilters, ", ")) os.Exit(1) } // Get defined active_filters activeFilters := viper.GetStringSlice("active_filters") if len(activeFilters) == 0 { activeFilters = coreFilters } mergedFilters := append(activeFilters, inputFilters...) uniqueFilters, dupplicatedFilters := util.RemoveDuplicates(mergedFilters) // Verify dupplicate if len(dupplicatedFilters) != 0 { color.Red("Duplicate filters found: %s", strings.Join(dupplicatedFilters, ", ")) os.Exit(1) } viper.Set("active_filters", uniqueFilters) if err := viper.WriteConfig(); err != nil { color.Red("Error writing config file: %s", err.Error()) os.Exit(1) } color.Green("Filter %s added", strings.Join(inputFilters, ", ")) }, } ================================================ FILE: cmd/filters/filters.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package filters import ( "github.com/spf13/cobra" ) var FiltersCmd = &cobra.Command{ Use: "filters", Aliases: []string{"filter"}, Short: "Manage filters for analyzing Kubernetes resources", Long: `The filters command allows you to manage filters that are used to analyze Kubernetes resources. You can list available filters to analyze resources.`, Run: func(cmd *cobra.Command, args []string) { if len(args) == 0 { _ = cmd.Help() return } }, } func init() { FiltersCmd.AddCommand(listCmd) FiltersCmd.AddCommand(addCmd) FiltersCmd.AddCommand(removeCmd) } ================================================ FILE: cmd/filters/list.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package filters import ( "fmt" "slices" "github.com/fatih/color" "github.com/k8sgpt-ai/k8sgpt/pkg/analyzer" "github.com/k8sgpt-ai/k8sgpt/pkg/integration" "github.com/k8sgpt-ai/k8sgpt/pkg/util" "github.com/spf13/cobra" "github.com/spf13/viper" ) var listCmd = &cobra.Command{ Use: "list", Short: "List available filters", Long: `The list command displays a list of available filters that can be used to analyze Kubernetes resources.`, Run: func(cmd *cobra.Command, args []string) { activeFilters := viper.GetStringSlice("active_filters") coreFilters, additionalFilters, integrationFilters := analyzer.ListFilters() integration := integration.NewIntegration() availableFilters := append(append(coreFilters, additionalFilters...), integrationFilters...) if len(activeFilters) == 0 { activeFilters = coreFilters } inactiveFilters := util.SliceDiff(availableFilters, activeFilters) fmt.Print(color.YellowString("Active: \n")) for _, filter := range activeFilters { // if the filter is an integration, mark this differently // but if the integration is inactive, remove if slices.Contains(integrationFilters, filter) { fmt.Printf("> %s\n", color.BlueString("%s (integration)", filter)) } else { // This strange bit of logic will loop through every integration via // OwnsAnalyzer subcommand to check the filter and as the integrationFilters... // was no match, we know this isn't part of an active integration if _, err := integration.AnalyzerByIntegration(filter); err != nil { fmt.Printf("> %s\n", color.GreenString(filter)) } } } // display inactive filters if len(inactiveFilters) != 0 { fmt.Print(color.YellowString("Unused: \n")) for _, filter := range inactiveFilters { // if the filter is an integration, mark this differently if slices.Contains(integrationFilters, filter) { fmt.Printf("> %s\n", color.BlueString("%s (integration)", filter)) } else { fmt.Printf("> %s\n", color.RedString(filter)) } } } }, } ================================================ FILE: cmd/filters/remove.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package filters import ( "os" "strings" "github.com/fatih/color" "github.com/k8sgpt-ai/k8sgpt/pkg/analyzer" "github.com/k8sgpt-ai/k8sgpt/pkg/util" "github.com/spf13/cobra" "github.com/spf13/viper" ) var removeCmd = &cobra.Command{ Use: "remove [filter(s)]", Short: "Remove one or more filters.", Long: `The add command remove one or more filters to the default set of filters used by the analyze.`, Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { inputFilters := strings.Split(args[0], ",") // Get defined active_filters activeFilters := viper.GetStringSlice("active_filters") coreFilters, _, _ := analyzer.ListFilters() if len(activeFilters) == 0 { activeFilters = coreFilters } // Check if input input filters is not empty for _, f := range inputFilters { if f == "" { color.Red("Filter cannot be empty. Please use correct syntax.") os.Exit(1) } } // verify dupplicate filters example: k8sgpt filters remove Pod Pod uniqueFilters, dupplicatedFilters := util.RemoveDuplicates(inputFilters) if len(dupplicatedFilters) != 0 { color.Red("Duplicate filters found: %s", strings.Join(dupplicatedFilters, ", ")) os.Exit(1) } // Verify if filter exist in config file and update default_filter filterNotFound := []string{} for _, filter := range uniqueFilters { foundFilter := false for i, f := range activeFilters { if f == filter { foundFilter = true activeFilters = append(activeFilters[:i], activeFilters[i+1:]...) break } } if !foundFilter { filterNotFound = append(filterNotFound, filter) } } if len(filterNotFound) != 0 { color.Red("Filter(s) %s does not exist in configuration file. Please use k8sgpt filters add.", strings.Join(filterNotFound, ", ")) os.Exit(1) } viper.Set("active_filters", activeFilters) if err := viper.WriteConfig(); err != nil { color.Red("Error writing config file: %s", err.Error()) os.Exit(1) } color.Green("Filter(s) %s removed", strings.Join(inputFilters, ", ")) }, } ================================================ FILE: cmd/generate/generate.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package generate import ( "fmt" "os/exec" "runtime" "github.com/fatih/color" "github.com/spf13/cobra" "github.com/spf13/viper" ) var ( backend string backendType string ) // generateCmd represents the auth command var GenerateCmd = &cobra.Command{ Use: "generate", Short: "Generate Key for your chosen backend (opens browser)", Long: `Opens your browser to generate a key for your chosen backend.`, Run: func(cmd *cobra.Command, args []string) { backendType = viper.GetString("backend_type") if backendType == "" { // Set the default backend backend = "openai" } // override the default backend if a flag is provided if backend != "" { backendType = backend } fmt.Println("") openbrowser("https://platform.openai.com/api-keys") }, } func init() { // add flag for backend GenerateCmd.Flags().StringVarP(&backend, "backend", "b", "openai", "Backend AI provider") } func openbrowser(url string) { var err error isGui := true switch runtime.GOOS { case "linux": _, err = exec.LookPath("xdg-open") if err != nil { isGui = false } else { err = exec.Command("xdg-open", url).Start() } case "windows": err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start() case "darwin": err = exec.Command("open", url).Start() default: err = fmt.Errorf("unsupported platform") } printInstructions(isGui, backend) if err != nil { fmt.Println(err) } } func printInstructions(isGui bool, backendType string) { fmt.Println("") if isGui { color.Green("Opening: https://platform.openai.com/api-keys to generate a key for %s", backendType) fmt.Println("") } else { color.Green("Please open: https://platform.openai.com/api-keys to generate a key for %s", backendType) fmt.Println("") } color.Green("Please copy the generated key and run `k8sgpt auth add` to add it to your config file") fmt.Println("") } ================================================ FILE: cmd/integration/activate.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package integration import ( "github.com/fatih/color" "github.com/k8sgpt-ai/k8sgpt/pkg/analyzer" "github.com/k8sgpt-ai/k8sgpt/pkg/integration" "github.com/spf13/cobra" "github.com/spf13/viper" ) var skipInstall bool // activateCmd represents the activate command var activateCmd = &cobra.Command{ Use: "activate [integration]", Short: "Activate an integration", Long: ``, Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { integrationName := args[0] coreFilters, _, _ := analyzer.ListFilters() // Update filters activeFilters := viper.GetStringSlice("active_filters") if len(activeFilters) == 0 { activeFilters = coreFilters } integration := integration.NewIntegration() // Check if the integation exists err := integration.Activate(integrationName, namespace, activeFilters, skipInstall) if err != nil { color.Red("Error: %v", err) return } color.Green("Activated integration %s", integrationName) }, } func init() { IntegrationCmd.AddCommand(activateCmd) 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)") } ================================================ FILE: cmd/integration/deactivate.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package integration import ( "github.com/fatih/color" "github.com/k8sgpt-ai/k8sgpt/pkg/integration" "github.com/spf13/cobra" ) // deactivateCmd represents the deactivate command var deactivateCmd = &cobra.Command{ Use: "deactivate [integration]", Short: "Deactivate an integration", Args: cobra.ExactArgs(1), Long: `For example e.g. k8sgpt integration deactivate prometheus`, Run: func(cmd *cobra.Command, args []string) { integrationName := args[0] integration := integration.NewIntegration() if err := integration.Deactivate(integrationName, namespace); err != nil { color.Red("Error: %v", err) return } color.Green("Deactivated integration %s", integrationName) }, } func init() { IntegrationCmd.AddCommand(deactivateCmd) } ================================================ FILE: cmd/integration/integration.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package integration import ( "github.com/spf13/cobra" ) var ( namespace string ) // IntegrationCmd represents the integrate command var IntegrationCmd = &cobra.Command{ Use: "integration", Aliases: []string{"integrations"}, Short: "Integrate another tool into K8sGPT", Long: `Integrate another tool into K8sGPT. For example: k8sgpt integration activate prometheus This would allow you to connect to prometheus running with your cluster.`, Run: func(cmd *cobra.Command, args []string) { _ = cmd.Help() }, } func init() { IntegrationCmd.PersistentFlags().StringVarP(&namespace, "namespace", "n", "default", "The namespace to use for the integration") } ================================================ FILE: cmd/integration/list.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package integration import ( "fmt" "os" "github.com/fatih/color" "github.com/k8sgpt-ai/k8sgpt/pkg/integration" "github.com/spf13/cobra" ) // listCmd represents the list command var listCmd = &cobra.Command{ Use: "list", Short: "Lists built-in integrations", Long: ``, Run: func(cmd *cobra.Command, args []string) { integrationProvider := integration.NewIntegration() integrations := integrationProvider.List() fmt.Println(color.YellowString("Active:")) for _, i := range integrations { b, err := integrationProvider.IsActivate(i) if err != nil { fmt.Println(err) os.Exit(1) } if b { fmt.Printf("> %s\n", color.GreenString(i)) } } fmt.Println(color.YellowString("Unused: ")) for _, i := range integrations { b, err := integrationProvider.IsActivate(i) if err != nil { fmt.Println(err) os.Exit(1) } if !b { fmt.Printf("> %s\n", color.GreenString(i)) } } }, } func init() { IntegrationCmd.AddCommand(listCmd) } ================================================ FILE: cmd/root.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cmd import ( "fmt" "os" "path/filepath" "github.com/adrg/xdg" "github.com/k8sgpt-ai/k8sgpt/cmd/analyze" "github.com/k8sgpt-ai/k8sgpt/cmd/auth" "github.com/k8sgpt-ai/k8sgpt/cmd/cache" customanalyzer "github.com/k8sgpt-ai/k8sgpt/cmd/customAnalyzer" "github.com/k8sgpt-ai/k8sgpt/cmd/dump" "github.com/k8sgpt-ai/k8sgpt/cmd/filters" "github.com/k8sgpt-ai/k8sgpt/cmd/generate" "github.com/k8sgpt-ai/k8sgpt/cmd/integration" "github.com/k8sgpt-ai/k8sgpt/cmd/serve" "github.com/k8sgpt-ai/k8sgpt/pkg/util" "github.com/spf13/cobra" "github.com/spf13/viper" ) var ( cfgFile string kubecontext string kubeconfig string verbose bool Version string Commit string Date string ) // rootCmd represents the base command when called without any subcommands var rootCmd = &cobra.Command{ Use: "k8sgpt", Short: "Kubernetes debugging powered by AI", Long: ``, // Uncomment the following line if your bare application // has an action associated with it: // Run: func(cmd *cobra.Command, args []string) { }, } // Execute adds all child commands to the root command and sets flags appropriately. // This is called by main.main(). It only needs to happen once to the rootCmd. func Execute(v string, c string, d string) { Version = v Commit = c Date = d viper.Set("Version", Version) viper.Set("Commit", Commit) viper.Set("Date", Date) err := rootCmd.Execute() if err != nil { os.Exit(1) } } func init() { performConfigMigrationIfNeeded() cobra.OnInitialize(initConfig) rootCmd.AddCommand(auth.AuthCmd) rootCmd.AddCommand(analyze.AnalyzeCmd) rootCmd.AddCommand(dump.DumpCmd) rootCmd.AddCommand(filters.FiltersCmd) rootCmd.AddCommand(generate.GenerateCmd) rootCmd.AddCommand(integration.IntegrationCmd) rootCmd.AddCommand(serve.ServeCmd) rootCmd.AddCommand(cache.CacheCmd) rootCmd.AddCommand(customanalyzer.CustomAnalyzerCmd) rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", fmt.Sprintf("Default config file (%s/k8sgpt/k8sgpt.yaml)", xdg.ConfigHome)) rootCmd.PersistentFlags().StringVar(&kubecontext, "kubecontext", "", "Kubernetes context to use. Only required if out-of-cluster.") rootCmd.PersistentFlags().StringVar(&kubeconfig, "kubeconfig", "", "Path to a kubeconfig. Only required if out-of-cluster.") rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Show detailed tool actions (e.g., API calls, checks).") } // initConfig reads in config file and ENV variables if set. func initConfig() { if cfgFile != "" { // Use config file from the flag. viper.SetConfigFile(cfgFile) } else { // the config will belocated under `~/.config/k8sgpt/k8sgpt.yaml` on linux configDir := filepath.Join(xdg.ConfigHome, "k8sgpt") viper.AddConfigPath(configDir) viper.SetConfigType("yaml") viper.SetConfigName("k8sgpt") _ = viper.SafeWriteConfig() } viper.Set("kubecontext", kubecontext) viper.Set("kubeconfig", kubeconfig) viper.Set("verbose", verbose) viper.SetEnvPrefix("K8SGPT") viper.AutomaticEnv() // read in environment variables that match // If a config file is found, read it in. if err := viper.ReadInConfig(); err == nil { _ = 1 // fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed()) } } func performConfigMigrationIfNeeded() { oldConfig, err := getLegacyConfigFilePath() cobra.CheckErr(err) oldConfigExists, err := util.FileExists(oldConfig) cobra.CheckErr(err) newConfig := getConfigFilePath() newConfigExists, err := util.FileExists(newConfig) cobra.CheckErr(err) configDir := filepath.Dir(newConfig) err = util.EnsureDirExists(configDir) cobra.CheckErr(err) if oldConfigExists && !newConfigExists { err = os.Rename(oldConfig, newConfig) cobra.CheckErr(err) } } func getConfigFilePath() string { return filepath.Join(xdg.ConfigHome, "k8sgpt", "k8sgpt.yaml") } func getLegacyConfigFilePath() (string, error) { home, err := os.UserHomeDir() if err != nil { return "", err } return filepath.Join(home, ".k8sgpt.yaml"), nil } ================================================ FILE: cmd/root_test.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cmd import ( "testing" "github.com/spf13/viper" ) // Test that verbose flag is correctly set in viper. func TestInitConfig_VerboseFlag(t *testing.T) { verbose = true viper.Reset() initConfig() if !viper.GetBool("verbose") { t.Error("Expected verbose flag to be true") } } ================================================ FILE: cmd/serve/serve.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package serve import ( "os" "strconv" k8sgptserver "github.com/k8sgpt-ai/k8sgpt/pkg/server" "github.com/fatih/color" "github.com/k8sgpt-ai/k8sgpt/pkg/ai" "github.com/spf13/cobra" "github.com/spf13/viper" "go.uber.org/zap" ) const ( defaultTemperature float32 = 0.7 defaultTopP float32 = 1.0 defaultTopK int32 = 50 defaultMaxTokens int = 2048 ) var ( port string metricsPort string backend string enableHttp bool enableMCP bool mcpPort string mcpHTTP bool // filters can be injected into the server (repeatable flag) filters []string ) var ServeCmd = &cobra.Command{ Use: "serve", Short: "Runs k8sgpt as a server", Long: `Runs k8sgpt as a server to allow for easy integration with other applications.`, Run: func(cmd *cobra.Command, args []string) { var configAI ai.AIConfiguration err := viper.UnmarshalKey("ai", &configAI) if err != nil { color.Red("Error: %v", err) os.Exit(1) } var aiProvider *ai.AIProvider if len(configAI.Providers) == 0 { // we validate and set temperature for our backend temperature := func() float32 { env := os.Getenv("K8SGPT_TEMPERATURE") if env == "" { return defaultTemperature } temperature, err := strconv.ParseFloat(env, 32) if err != nil { color.Red("Unable to convert Temperature value: %v", err) os.Exit(1) } if temperature > 1.0 || temperature < 0.0 { color.Red("Error: temperature ranges from 0 to 1.") os.Exit(1) } return float32(temperature) } topP := func() float32 { env := os.Getenv("K8SGPT_TOP_P") if env == "" { return defaultTopP } topP, err := strconv.ParseFloat(env, 32) if err != nil { color.Red("Unable to convert topP value: %v", err) os.Exit(1) } if topP > 1.0 || topP < 0.0 { color.Red("Error: topP ranges from 0 to 1.") os.Exit(1) } return float32(topP) } topK := func() int32 { env := os.Getenv("K8SGPT_TOP_K") if env == "" { return defaultTopK } topK, err := strconv.ParseFloat(env, 32) if err != nil { color.Red("Unable to convert topK value: %v", err) os.Exit(1) } if topK < 10 || topK > 100 { color.Red("Error: topK ranges from 1 to 100.") os.Exit(1) } return int32(topK) } maxTokens := func() int { env := os.Getenv("K8SGPT_MAX_TOKENS") if env == "" { return defaultMaxTokens } maxTokens, err := strconv.ParseInt(env, 10, 32) if err != nil { color.Red("Unable to convert maxTokens value: %v", err) os.Exit(1) } return int(maxTokens) } // Check for env injection backend = os.Getenv("K8SGPT_BACKEND") password := os.Getenv("K8SGPT_PASSWORD") model := os.Getenv("K8SGPT_MODEL") baseURL := os.Getenv("K8SGPT_BASEURL") engine := os.Getenv("K8SGPT_ENGINE") proxyEndpoint := os.Getenv("K8SGPT_PROXY_ENDPOINT") providerId := os.Getenv("K8SGPT_PROVIDER_ID") // If the envs are set, allocate in place to the aiProvider // else exit with error envIsSet := backend != "" || password != "" || model != "" if envIsSet { aiProvider = &ai.AIProvider{ Name: backend, Password: password, Model: model, BaseURL: baseURL, Engine: engine, ProxyEndpoint: proxyEndpoint, ProviderId: providerId, Temperature: temperature(), TopP: topP(), TopK: topK(), MaxTokens: maxTokens(), } configAI.Providers = append(configAI.Providers, *aiProvider) viper.Set("ai", configAI) if err := viper.WriteConfig(); err != nil { color.Red("Error writing config file: %s", err.Error()) os.Exit(1) } } else { color.Red("Error: AI provider not specified in configuration. Please run k8sgpt auth") os.Exit(1) } } if aiProvider == nil { for _, provider := range configAI.Providers { if backend == provider.Name { // the pointer to the range variable is not really an issue here, as there // is a break right after, but to prevent potential future issues, a temp // variable is assigned p := provider aiProvider = &p break } } } if aiProvider == nil || aiProvider.Name == "" { color.Red("Error: AI provider %s not specified in configuration. Please run k8sgpt auth", backend) os.Exit(1) } logger, err := zap.NewProduction() if err != nil { color.Red("failed to create logger: %v", err) os.Exit(1) } defer func() { if err := logger.Sync(); err != nil { color.Red("failed to sync logger: %v", err) os.Exit(1) } }() if enableMCP { // Create and start MCP server mcpServer, err := k8sgptserver.NewMCPServer(mcpPort, aiProvider, mcpHTTP, logger) if err != nil { color.Red("Error creating MCP server: %v", err) os.Exit(1) } go func() { if err := mcpServer.Start(); err != nil { color.Red("Error starting MCP server: %v", err) os.Exit(1) } }() } // Allow metrics port to be overridden by environment variable if envMetricsPort := os.Getenv("K8SGPT_METRICS_PORT"); envMetricsPort != "" && !cmd.Flags().Changed("metrics-port") { metricsPort = envMetricsPort } server := k8sgptserver.Config{ Backend: aiProvider.Name, Port: port, MetricsPort: metricsPort, EnableHttp: enableHttp, Token: aiProvider.Password, Logger: logger, Filters: filters, } go func() { if err := server.ServeMetrics(); err != nil { color.Red("Error: %v", err) os.Exit(1) } }() go func() { if err := server.Serve(); err != nil { color.Red("Error: %v", err) os.Exit(1) } }() // Wait for both servers to exit select {} }, } func init() { // add flag for backend ServeCmd.Flags().StringVarP(&port, "port", "p", "8080", "Port to run the server on") ServeCmd.Flags().StringVarP(&metricsPort, "metrics-port", "m", "8081", "Port to run the metrics-server on (env: K8SGPT_METRICS_PORT)") ServeCmd.Flags().StringVarP(&backend, "backend", "b", "openai", "Backend AI provider") ServeCmd.Flags().BoolVarP(&enableHttp, "http", "", false, "Enable REST/http using gppc-gateway") ServeCmd.Flags().BoolVarP(&enableMCP, "mcp", "", false, "Enable Mission Control Protocol server") ServeCmd.Flags().StringVarP(&mcpPort, "mcp-port", "", "8089", "Port to run the MCP server on") ServeCmd.Flags().BoolVarP(&mcpHTTP, "mcp-http", "", false, "Enable HTTP mode for MCP server") // allow injecting filters into the running server (repeatable) ServeCmd.Flags().StringSliceVar(&filters, "filter", []string{}, "Filter to apply (can be specified multiple times)") } ================================================ FILE: cmd/version.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cmd import ( "fmt" "runtime/debug" "github.com/spf13/cobra" ) // versionCmd represents the version command var versionCmd = &cobra.Command{ Use: "version", Short: "Print the version number of k8sgpt", Long: `All software has versions. This is k8sgpt's`, Run: func(cmd *cobra.Command, args []string) { if Version == "dev" { details, ok := debug.ReadBuildInfo() if ok && details.Main.Version != "" && details.Main.Version != "(devel)" { Version = details.Main.Version for _, i := range details.Settings { if i.Key == "vcs.time" { Date = i.Value } if i.Key == "vcs.revision" { Commit = i.Value } } } } fmt.Printf("k8sgpt: %s (%s), built at: %s\n", Version, Commit, Date) }, } func init() { rootCmd.AddCommand(versionCmd) } ================================================ FILE: container/Dockerfile ================================================ # Copyright 2023 The K8sGPT Authors. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # http://www.apache.org/licenses/LICENSE-2.0 # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. FROM golang:1.24-alpine3.23 AS builder ENV CGO_ENABLED=0 ARG VERSION ARG COMMIT ARG DATE WORKDIR /workspace COPY go.mod go.sum ./ RUN go mod download COPY ./ ./ RUN go build -o /workspace/k8sgpt -ldflags "-X main.version=${VERSION} -X main.commit=${COMMIT} -X main.date=${DATE}" ./ FROM gcr.io/distroless/static AS production LABEL org.opencontainers.image.source="https://github.com/k8sgpt-ai/k8sgpt" \ org.opencontainers.image.url="https://k8sgpt.ai" \ org.opencontainers.image.title="k8sgpt" \ org.opencontainers.image.vendor='The K8sGPT Authors' \ org.opencontainers.image.licenses='Apache-2.0' WORKDIR / COPY --from=builder /workspace/k8sgpt . USER 65532:65532 ENTRYPOINT ["/k8sgpt"] ================================================ FILE: go.mod ================================================ module github.com/k8sgpt-ai/k8sgpt go 1.24.1 toolchain go1.24.11 require ( github.com/fatih/color v1.18.0 github.com/kedacore/keda/v2 v2.16.0 github.com/magiconair/properties v1.8.9 github.com/mittwald/go-helm-client v0.12.14 github.com/ollama/ollama v0.13.4 github.com/sashabaranov/go-openai v1.36.0 github.com/schollz/progressbar/v3 v3.17.1 github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.10.0 golang.org/x/term v0.33.0 helm.sh/helm/v3 v3.17.4 k8s.io/api v0.32.3 k8s.io/apimachinery v0.32.3 k8s.io/client-go v0.32.3 k8s.io/kubectl v0.32.2 // indirect ) require github.com/adrg/xdg v0.5.3 require ( buf.build/gen/go/interplex-ai/schemas/grpc/go v1.5.1-20241117203254-a91193b62179.1 buf.build/gen/go/interplex-ai/schemas/protocolbuffers/go v1.35.2-20241117203254-a91193b62179.1 buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc-ecosystem/gateway/v2 v2.24.0-20241118152629-1379a5a1889d.1 buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go v1.5.1-20241118152629-1379a5a1889d.1 buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go v1.35.2-20241118152629-1379a5a1889d.1 cloud.google.com/go/storage v1.50.0 cloud.google.com/go/vertexai v0.13.2 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.5.0 github.com/IBM/watsonx-go v1.0.1 github.com/agiledragon/gomonkey/v2 v2.13.0 github.com/aws/aws-sdk-go v1.55.7 github.com/aws/aws-sdk-go-v2 v1.36.3 github.com/aws/aws-sdk-go-v2/config v1.29.14 github.com/aws/aws-sdk-go-v2/service/bedrock v1.33.0 github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.30.0 github.com/aws/smithy-go v1.22.2 github.com/cohere-ai/cohere-go/v2 v2.12.2 github.com/go-logr/zapr v1.3.0 github.com/google/generative-ai-go v0.19.0 github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 github.com/hupe1980/go-huggingface v0.0.15 github.com/kyverno/policy-reporter-kyverno-plugin v1.6.4 github.com/mark3labs/mcp-go v0.36.0 github.com/olekukonko/tablewriter v0.0.5 github.com/oracle/oci-go-sdk/v65 v65.79.0 github.com/prometheus/prometheus v0.306.0 github.com/pterm/pterm v0.12.80 google.golang.org/api v0.239.0 gopkg.in/yaml.v2 v2.4.0 sigs.k8s.io/controller-runtime v0.19.3 sigs.k8s.io/gateway-api v1.2.1 ) require ( atomicgo.dev/cursor v0.2.0 // indirect atomicgo.dev/keyboard v0.2.9 // indirect atomicgo.dev/schedule v0.1.0 // indirect cel.dev/expr v0.23.0 // indirect cloud.google.com/go v0.120.0 // indirect cloud.google.com/go/ai v0.8.0 // indirect cloud.google.com/go/aiplatform v1.85.0 // indirect cloud.google.com/go/auth v0.16.2 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.7.0 // indirect cloud.google.com/go/iam v1.5.2 // indirect cloud.google.com/go/longrunning v0.6.7 // indirect cloud.google.com/go/monitoring v1.24.2 // indirect dario.cat/mergo v1.0.1 // indirect github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.50.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.50.0 // indirect github.com/Microsoft/hcsshim v0.12.4 // indirect github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.17.67 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/buger/jsonparser v1.1.1 // indirect github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f // indirect github.com/containerd/console v1.0.4 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect github.com/creack/pty v1.1.21 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect github.com/evanphx/json-patch/v5 v5.9.0 // indirect github.com/expr-lang/expr v1.17.7 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/go-jose/go-jose/v4 v4.0.5 // indirect github.com/gofrs/flock v0.12.1 // indirect github.com/golang-jwt/jwt/v5 v5.2.2 // indirect github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect github.com/googleapis/gax-go/v2 v2.14.2 // indirect github.com/gookit/color v1.5.4 // indirect github.com/gorilla/websocket v1.5.1 // indirect github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect github.com/invopop/jsonschema v0.13.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/lithammer/fuzzysearch v1.1.8 // indirect github.com/moby/sys/mountinfo v0.7.1 // indirect github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/prometheus/sigv4 v0.2.0 // indirect github.com/sagikazarmark/locafero v0.6.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/segmentio/fasthash v1.0.3 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/sony/gobreaker v0.5.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect github.com/zeebo/errs v1.4.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/detectors/gcp v1.35.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect go.opentelemetry.io/otel/metric v1.36.0 // indirect go.opentelemetry.io/otel/sdk v1.36.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.36.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect knative.dev/pkg v0.0.0-20241026180704-25f6002b00f3 // indirect ) require ( github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect github.com/MakeNowJust/heredoc v1.0.0 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.3.0 // indirect github.com/Masterminds/sprig/v3 v3.3.0 // indirect github.com/Masterminds/squirrel v1.5.4 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chai2010/gettext-go v1.0.3 // indirect github.com/containerd/containerd v1.7.29 // indirect github.com/cyphar/filepath-securejoin v0.3.6 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/docker/cli v26.1.4+incompatible // indirect github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/docker v28.3.0+incompatible // indirect github.com/docker/docker-credential-helpers v0.8.2 // indirect github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-metrics v0.0.1 // indirect github.com/emicklei/go-restful/v3 v3.12.1 // indirect github.com/evanphx/json-patch v5.9.0+incompatible // indirect github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/go-errors/errors v1.5.1 // indirect github.com/go-gorp/gorp/v3 v3.1.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/btree v1.1.2 // indirect github.com/google/gnostic v0.7.0 github.com/google/go-cmp v0.7.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/gosuri/uitable v0.0.4 // indirect github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/huandu/xstrings v1.5.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jmoiron/sqlx v1.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/lib/pq v1.10.9 // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/moby/locker v1.0.1 // indirect github.com/moby/spdystream v0.5.0 // indirect github.com/moby/term v0.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.23.0-rc.1 github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.65.1-0.20250703115700-7f8b2a0d32d3 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/robfig/cron/v3 v3.0.1 github.com/rubenv/sql-migrate v1.7.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.7.1 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/xlab/treeprint v1.2.0 // indirect go.opentelemetry.io/otel v1.36.0 // indirect go.opentelemetry.io/otel/trace v1.36.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 golang.org/x/crypto v0.40.0 // indirect golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa // indirect golang.org/x/net v0.42.0 golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/sync v0.16.0 // indirect golang.org/x/sys v0.36.0 // indirect golang.org/x/text v0.27.0 // indirect golang.org/x/time v0.12.0 // indirect google.golang.org/grpc v1.73.0 google.golang.org/protobuf v1.36.6 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect k8s.io/apiextensions-apiserver v0.32.2 k8s.io/apiserver v0.32.2 // indirect k8s.io/cli-runtime v0.32.2 // indirect k8s.io/component-base v0.32.2 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 oras.land/oras-go v1.2.5 // indirect sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect sigs.k8s.io/kustomize/api v0.18.0 // indirect sigs.k8s.io/kustomize/kyaml v0.18.1 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect sigs.k8s.io/yaml v1.4.0 // indirect ) // v1.2.0 is taken from github.com/open-policy-agent/opa v0.42.0 // v1.2.0 incompatible with github.com/docker/docker v23.0.0-rc.1+incompatible //replace oras.land/oras-go => oras.land/oras-go v1.2.4 replace github.com/docker/docker => github.com/docker/docker v28.0.4+incompatible replace dario.cat/mergo => github.com/imdario/mergo v1.0.1 ================================================ FILE: go.sum ================================================ atomicgo.dev/assert v0.0.2 h1:FiKeMiZSgRrZsPo9qn/7vmr7mCsh5SZyXY4YGYiYwrg= atomicgo.dev/assert v0.0.2/go.mod h1:ut4NcI3QDdJtlmAxQULOmA13Gz6e2DWbSAS8RUOmNYQ= atomicgo.dev/cursor v0.2.0 h1:H6XN5alUJ52FZZUkI7AlJbUc1aW38GWZalpYRPpoPOw= atomicgo.dev/cursor v0.2.0/go.mod h1:Lr4ZJB3U7DfPPOkbH7/6TOtJ4vFGHlgj1nc+n900IpU= atomicgo.dev/keyboard v0.2.9 h1:tOsIid3nlPLZ3lwgG8KZMp/SFmr7P0ssEN5JUsm78K8= atomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ= atomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs= atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU= buf.build/gen/go/interplex-ai/schemas/grpc/go v1.5.1-20241117203254-a91193b62179.1 h1:v8dSqv/mIjimtO6c1FwknMWW1D7BXK3VPToy/UHpqkY= buf.build/gen/go/interplex-ai/schemas/grpc/go v1.5.1-20241117203254-a91193b62179.1/go.mod h1:+AL6UMLDgY2rP2ZgM3tlDBnemFklPzUX1rtR7MJKmM4= buf.build/gen/go/interplex-ai/schemas/protocolbuffers/go v1.35.2-20241117203254-a91193b62179.1 h1:DTYmvB5P/Dc1oL3oS75pu8bkGcSGgATGhVxqB0uMkPw= buf.build/gen/go/interplex-ai/schemas/protocolbuffers/go v1.35.2-20241117203254-a91193b62179.1/go.mod h1:UeiXHRr2O8y37OLckIQilBNcy22+u41dADFpEXeUH+I= buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc-ecosystem/gateway/v2 v2.24.0-20241118152629-1379a5a1889d.1 h1:9r9t2pVf+X8oesYpfTATH9FGIWbVy70eExkEQWj/qrA= buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc-ecosystem/gateway/v2 v2.24.0-20241118152629-1379a5a1889d.1/go.mod h1:M+KYheBX0z4c6yvFj2WUmr/Qs1KtxwsecD6Hv9SUo/s= buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go v1.5.1-20241118152629-1379a5a1889d.1 h1:thB3o5jG9fU30xDbC5+PC3DLyB8/TjGtObhvC/A+AK0= buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go v1.5.1-20241118152629-1379a5a1889d.1/go.mod h1:rlbkTkVN2P3aNR0U/7N5d9/uvNW8/dzHwtJDfPzh2vc= buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go v1.35.2-20241118152629-1379a5a1889d.1 h1:Z+fW0kWryP6LdjP+z+d1/WT4tObrq890aye4aPIh6hM= buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go v1.35.2-20241118152629-1379a5a1889d.1/go.mod h1:dqopmdpTDT6p9kPTxVCgR8WDnNb1SjZjwzaNj/kRbps= cel.dev/expr v0.23.0 h1:wUb94w6OYQS4uXraxo9U+wUAs9jT47Xvl4iPgAwM2ss= cel.dev/expr v0.23.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= cloud.google.com/go v0.100.1/go.mod h1:fs4QogzfH5n2pBXBP9vRiU+eCny7lD2vmFZy79Iuw1U= cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU= cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA= cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM= cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I= cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY= cloud.google.com/go v0.120.0 h1:wc6bgG9DHyKqF5/vQvX1CiZrtHnxJjBlKUyF9nP6meA= cloud.google.com/go v0.120.0/go.mod h1:/beW32s8/pGRuj4IILWQNd4uuebeT4dkOhKmkfit64Q= cloud.google.com/go/accessapproval v1.4.0/go.mod h1:zybIuC3KpDOvotz59lFe5qxRZx6C75OtwbisN56xYB4= cloud.google.com/go/accessapproval v1.5.0/go.mod h1:HFy3tuiGvMdcd/u+Cu5b9NkO1pEICJ46IR82PoUdplw= cloud.google.com/go/accessapproval v1.6.0/go.mod h1:R0EiYnwV5fsRFiKZkPHr6mwyk2wxUJ30nL4j2pcFY2E= cloud.google.com/go/accesscontextmanager v1.3.0/go.mod h1:TgCBehyr5gNMz7ZaH9xubp+CE8dkrszb4oK9CWyvD4o= cloud.google.com/go/accesscontextmanager v1.4.0/go.mod h1:/Kjh7BBu/Gh83sv+K60vN9QE5NJcd80sU33vIe2IFPE= cloud.google.com/go/accesscontextmanager v1.6.0/go.mod h1:8XCvZWfYw3K/ji0iVnp+6pu7huxoQTLmxAbVjbloTtM= cloud.google.com/go/accesscontextmanager v1.7.0/go.mod h1:CEGLewx8dwa33aDAZQujl7Dx+uYhS0eay198wB/VumQ= cloud.google.com/go/ai v0.8.0 h1:rXUEz8Wp2OlrM8r1bfmpF2+VKqc1VJpafE3HgzRnD/w= cloud.google.com/go/ai v0.8.0/go.mod h1:t3Dfk4cM61sytiggo2UyGsDVW3RF1qGZaUKDrZFyqkE= cloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw= cloud.google.com/go/aiplatform v1.24.0/go.mod h1:67UUvRBKG6GTayHKV8DBv2RtR1t93YRu5B1P3x99mYY= cloud.google.com/go/aiplatform v1.27.0/go.mod h1:Bvxqtl40l0WImSb04d0hXFU7gDOiq9jQmorivIiWcKg= cloud.google.com/go/aiplatform v1.35.0/go.mod h1:7MFT/vCaOyZT/4IIFfxH4ErVg/4ku6lKv3w0+tFTgXQ= cloud.google.com/go/aiplatform v1.36.1/go.mod h1:WTm12vJRPARNvJ+v6P52RDHCNe4AhvjcIZ/9/RRHy/k= cloud.google.com/go/aiplatform v1.37.0/go.mod h1:IU2Cv29Lv9oCn/9LkFiiuKfwrRTq+QQMbW+hPCxJGZw= cloud.google.com/go/aiplatform v1.85.0 h1:80/GqdP8Tovaaw9Qr6fYZNDvwJeA9rLk8mYkqBJNIJQ= cloud.google.com/go/aiplatform v1.85.0/go.mod h1:S4DIKz3TFLSt7ooF2aCRdAqsUR4v/YDXUoHqn5P0EFc= cloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI= cloud.google.com/go/analytics v0.12.0/go.mod h1:gkfj9h6XRf9+TS4bmuhPEShsh3hH8PAZzm/41OOhQd4= cloud.google.com/go/analytics v0.17.0/go.mod h1:WXFa3WSym4IZ+JiKmavYdJwGG/CvpqiqczmL59bTD9M= cloud.google.com/go/analytics v0.18.0/go.mod h1:ZkeHGQlcIPkw0R/GW+boWHhCOR43xz9RN/jn7WcqfIE= cloud.google.com/go/analytics v0.19.0/go.mod h1:k8liqf5/HCnOUkbawNtrWWc+UAzyDlW89doe8TtoDsE= cloud.google.com/go/apigateway v1.3.0/go.mod h1:89Z8Bhpmxu6AmUxuVRg/ECRGReEdiP3vQtk4Z1J9rJk= cloud.google.com/go/apigateway v1.4.0/go.mod h1:pHVY9MKGaH9PQ3pJ4YLzoj6U5FUDeDFBllIz7WmzJoc= cloud.google.com/go/apigateway v1.5.0/go.mod h1:GpnZR3Q4rR7LVu5951qfXPJCHquZt02jf7xQx7kpqN8= cloud.google.com/go/apigeeconnect v1.3.0/go.mod h1:G/AwXFAKo0gIXkPTVfZDd2qA1TxBXJ3MgMRBQkIi9jc= cloud.google.com/go/apigeeconnect v1.4.0/go.mod h1:kV4NwOKqjvt2JYR0AoIWo2QGfoRtn/pkS3QlHp0Ni04= cloud.google.com/go/apigeeconnect v1.5.0/go.mod h1:KFaCqvBRU6idyhSNyn3vlHXc8VMDJdRmwDF6JyFRqZ8= cloud.google.com/go/apigeeregistry v0.4.0/go.mod h1:EUG4PGcsZvxOXAdyEghIdXwAEi/4MEaoqLMLDMIwKXY= cloud.google.com/go/apigeeregistry v0.5.0/go.mod h1:YR5+s0BVNZfVOUkMa5pAR2xGd0A473vA5M7j247o1wM= cloud.google.com/go/apigeeregistry v0.6.0/go.mod h1:BFNzW7yQVLZ3yj0TKcwzb8n25CFBri51GVGOEUcgQsc= cloud.google.com/go/apikeys v0.4.0/go.mod h1:XATS/yqZbaBK0HOssf+ALHp8jAlNHUgyfprvNcBIszU= cloud.google.com/go/apikeys v0.5.0/go.mod h1:5aQfwY4D+ewMMWScd3hm2en3hCj+BROlyrt3ytS7KLI= cloud.google.com/go/apikeys v0.6.0/go.mod h1:kbpXu5upyiAlGkKrJgQl8A0rKNNJ7dQ377pdroRSSi8= cloud.google.com/go/appengine v1.4.0/go.mod h1:CS2NhuBuDXM9f+qscZ6V86m1MIIqPj3WC/UoEuR1Sno= cloud.google.com/go/appengine v1.5.0/go.mod h1:TfasSozdkFI0zeoxW3PTBLiNqRmzraodCWatWI9Dmak= cloud.google.com/go/appengine v1.6.0/go.mod h1:hg6i0J/BD2cKmDJbaFSYHFyZkgBEfQrDg/X0V5fJn84= cloud.google.com/go/appengine v1.7.0/go.mod h1:eZqpbHFCqRGa2aCdope7eC0SWLV1j0neb/QnMJVWx6A= cloud.google.com/go/appengine v1.7.1/go.mod h1:IHLToyb/3fKutRysUlFO0BPt5j7RiQ45nrzEJmKTo6E= cloud.google.com/go/area120 v0.5.0/go.mod h1:DE/n4mp+iqVyvxHN41Vf1CR602GiHQjFPusMFW6bGR4= cloud.google.com/go/area120 v0.6.0/go.mod h1:39yFJqWVgm0UZqWTOdqkLhjoC7uFfgXRC8g/ZegeAh0= cloud.google.com/go/area120 v0.7.0/go.mod h1:a3+8EUD1SX5RUcCs3MY5YasiO1z6yLiNLRiFrykbynY= cloud.google.com/go/area120 v0.7.1/go.mod h1:j84i4E1RboTWjKtZVWXPqvK5VHQFJRF2c1Nm69pWm9k= cloud.google.com/go/artifactregistry v1.6.0/go.mod h1:IYt0oBPSAGYj/kprzsBjZ/4LnG/zOcHyFHjWPCi6SAQ= cloud.google.com/go/artifactregistry v1.7.0/go.mod h1:mqTOFOnGZx8EtSqK/ZWcsm/4U8B77rbcLP6ruDU2Ixk= cloud.google.com/go/artifactregistry v1.8.0/go.mod h1:w3GQXkJX8hiKN0v+at4b0qotwijQbYUqF2GWkZzAhC0= cloud.google.com/go/artifactregistry v1.9.0/go.mod h1:2K2RqvA2CYvAeARHRkLDhMDJ3OXy26h3XW+3/Jh2uYc= cloud.google.com/go/artifactregistry v1.11.1/go.mod h1:lLYghw+Itq9SONbCa1YWBoWs1nOucMH0pwXN1rOBZFI= cloud.google.com/go/artifactregistry v1.11.2/go.mod h1:nLZns771ZGAwVLzTX/7Al6R9ehma4WUEhZGWV6CeQNQ= cloud.google.com/go/artifactregistry v1.12.0/go.mod h1:o6P3MIvtzTOnmvGagO9v/rOjjA0HmhJ+/6KAXrmYDCI= cloud.google.com/go/artifactregistry v1.13.0/go.mod h1:uy/LNfoOIivepGhooAUpL1i30Hgee3Cu0l4VTWHUC08= cloud.google.com/go/asset v1.5.0/go.mod h1:5mfs8UvcM5wHhqtSv8J1CtxxaQq3AdBxxQi2jGW/K4o= cloud.google.com/go/asset v1.7.0/go.mod h1:YbENsRK4+xTiL+Ofoj5Ckf+O17kJtgp3Y3nn4uzZz5s= cloud.google.com/go/asset v1.8.0/go.mod h1:mUNGKhiqIdbr8X7KNayoYvyc4HbbFO9URsjbytpUaW0= cloud.google.com/go/asset v1.9.0/go.mod h1:83MOE6jEJBMqFKadM9NLRcs80Gdw76qGuHn8m3h8oHQ= cloud.google.com/go/asset v1.10.0/go.mod h1:pLz7uokL80qKhzKr4xXGvBQXnzHn5evJAEAtZiIb0wY= cloud.google.com/go/asset v1.11.1/go.mod h1:fSwLhbRvC9p9CXQHJ3BgFeQNM4c9x10lqlrdEUYXlJo= cloud.google.com/go/asset v1.12.0/go.mod h1:h9/sFOa4eDIyKmH6QMpm4eUK3pDojWnUhTgJlk762Hg= cloud.google.com/go/asset v1.13.0/go.mod h1:WQAMyYek/b7NBpYq/K4KJWcRqzoalEsxz/t/dTk4THw= cloud.google.com/go/assuredworkloads v1.5.0/go.mod h1:n8HOZ6pff6re5KYfBXcFvSViQjDwxFkAkmUFffJRbbY= cloud.google.com/go/assuredworkloads v1.6.0/go.mod h1:yo2YOk37Yc89Rsd5QMVECvjaMKymF9OP+QXWlKXUkXw= cloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVoYoxeLBoj4XkKYscNI= cloud.google.com/go/assuredworkloads v1.8.0/go.mod h1:AsX2cqyNCOvEQC8RMPnoc0yEarXQk6WEKkxYfL6kGIo= cloud.google.com/go/assuredworkloads v1.9.0/go.mod h1:kFuI1P78bplYtT77Tb1hi0FMxM0vVpRC7VVoJC3ZoT0= cloud.google.com/go/assuredworkloads v1.10.0/go.mod h1:kwdUQuXcedVdsIaKgKTp9t0UJkE5+PAVNhdQm4ZVq2E= cloud.google.com/go/auth v0.16.2 h1:QvBAGFPLrDeoiNjyfVunhQ10HKNYuOwZ5noee0M5df4= cloud.google.com/go/auth v0.16.2/go.mod h1:sRBas2Y1fB1vZTdurouM0AzuYQBMZinrUYL8EufhtEA= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0= cloud.google.com/go/automl v1.6.0/go.mod h1:ugf8a6Fx+zP0D59WLhqgTDsQI9w07o64uf/Is3Nh5p8= cloud.google.com/go/automl v1.7.0/go.mod h1:RL9MYCCsJEOmt0Wf3z9uzG0a7adTT1fe+aObgSpkCt8= cloud.google.com/go/automl v1.8.0/go.mod h1:xWx7G/aPEe/NP+qzYXktoBSDfjO+vnKMGgsApGJJquM= cloud.google.com/go/automl v1.12.0/go.mod h1:tWDcHDp86aMIuHmyvjuKeeHEGq76lD7ZqfGLN6B0NuU= cloud.google.com/go/baremetalsolution v0.3.0/go.mod h1:XOrocE+pvK1xFfleEnShBlNAXf+j5blPPxrhjKgnIFc= cloud.google.com/go/baremetalsolution v0.4.0/go.mod h1:BymplhAadOO/eBa7KewQ0Ppg4A4Wplbn+PsFKRLo0uI= cloud.google.com/go/baremetalsolution v0.5.0/go.mod h1:dXGxEkmR9BMwxhzBhV0AioD0ULBmuLZI8CdwalUxuss= cloud.google.com/go/batch v0.3.0/go.mod h1:TR18ZoAekj1GuirsUsR1ZTKN3FC/4UDnScjT8NXImFE= cloud.google.com/go/batch v0.4.0/go.mod h1:WZkHnP43R/QCGQsZ+0JyG4i79ranE2u8xvjq/9+STPE= cloud.google.com/go/batch v0.7.0/go.mod h1:vLZN95s6teRUqRQ4s3RLDsH8PvboqBK+rn1oevL159g= cloud.google.com/go/beyondcorp v0.2.0/go.mod h1:TB7Bd+EEtcw9PCPQhCJtJGjk/7TC6ckmnSFS+xwTfm4= cloud.google.com/go/beyondcorp v0.3.0/go.mod h1:E5U5lcrcXMsCuoDNyGrpyTm/hn7ne941Jz2vmksAxW8= cloud.google.com/go/beyondcorp v0.4.0/go.mod h1:3ApA0mbhHx6YImmuubf5pyW8srKnCEPON32/5hj+RmM= cloud.google.com/go/beyondcorp v0.5.0/go.mod h1:uFqj9X+dSfrheVp7ssLTaRHd2EHqSL4QZmH4e8WXGGU= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/bigquery v1.42.0/go.mod h1:8dRTJxhtG+vwBKzE5OseQn/hiydoQN3EedCaOdYmxRA= cloud.google.com/go/bigquery v1.43.0/go.mod h1:ZMQcXHsl+xmU1z36G2jNGZmKp9zNY5BUua5wDgmNCfw= cloud.google.com/go/bigquery v1.44.0/go.mod h1:0Y33VqXTEsbamHJvJHdFmtqHvMIY28aK1+dFsvaChGc= cloud.google.com/go/bigquery v1.47.0/go.mod h1:sA9XOgy0A8vQK9+MWhEQTY6Tix87M/ZurWFIxmF9I/E= cloud.google.com/go/bigquery v1.48.0/go.mod h1:QAwSz+ipNgfL5jxiaK7weyOhzdoAy1zFm0Nf1fysJac= cloud.google.com/go/bigquery v1.49.0/go.mod h1:Sv8hMmTFFYBlt/ftw2uN6dFdQPzBlREY9yBh7Oy7/4Q= cloud.google.com/go/bigquery v1.50.0/go.mod h1:YrleYEh2pSEbgTBZYMJ5SuSr0ML3ypjRB1zgf7pvQLU= cloud.google.com/go/billing v1.4.0/go.mod h1:g9IdKBEFlItS8bTtlrZdVLWSSdSyFUZKXNS02zKMOZY= cloud.google.com/go/billing v1.5.0/go.mod h1:mztb1tBc3QekhjSgmpf/CV4LzWXLzCArwpLmP2Gm88s= cloud.google.com/go/billing v1.6.0/go.mod h1:WoXzguj+BeHXPbKfNWkqVtDdzORazmCjraY+vrxcyvI= cloud.google.com/go/billing v1.7.0/go.mod h1:q457N3Hbj9lYwwRbnlD7vUpyjq6u5U1RAOArInEiD5Y= cloud.google.com/go/billing v1.12.0/go.mod h1:yKrZio/eu+okO/2McZEbch17O5CB5NpZhhXG6Z766ss= cloud.google.com/go/billing v1.13.0/go.mod h1:7kB2W9Xf98hP9Sr12KfECgfGclsH3CQR0R08tnRlRbc= cloud.google.com/go/binaryauthorization v1.1.0/go.mod h1:xwnoWu3Y84jbuHa0zd526MJYmtnVXn0syOjaJgy4+dM= cloud.google.com/go/binaryauthorization v1.2.0/go.mod h1:86WKkJHtRcv5ViNABtYMhhNWRrD1Vpi//uKEy7aYEfI= cloud.google.com/go/binaryauthorization v1.3.0/go.mod h1:lRZbKgjDIIQvzYQS1p99A7/U1JqvqeZg0wiI5tp6tg0= cloud.google.com/go/binaryauthorization v1.4.0/go.mod h1:tsSPQrBd77VLplV70GUhBf/Zm3FsKmgSqgm4UmiDItk= cloud.google.com/go/binaryauthorization v1.5.0/go.mod h1:OSe4OU1nN/VswXKRBmciKpo9LulY41gch5c68htf3/Q= cloud.google.com/go/certificatemanager v1.3.0/go.mod h1:n6twGDvcUBFu9uBgt4eYvvf3sQ6My8jADcOVwHmzadg= cloud.google.com/go/certificatemanager v1.4.0/go.mod h1:vowpercVFyqs8ABSmrdV+GiFf2H/ch3KyudYQEMM590= cloud.google.com/go/certificatemanager v1.6.0/go.mod h1:3Hh64rCKjRAX8dXgRAyOcY5vQ/fE1sh8o+Mdd6KPgY8= cloud.google.com/go/channel v1.8.0/go.mod h1:W5SwCXDJsq/rg3tn3oG0LOxpAo6IMxNa09ngphpSlnk= cloud.google.com/go/channel v1.9.0/go.mod h1:jcu05W0my9Vx4mt3/rEHpfxc9eKi9XwsdDL8yBMbKUk= cloud.google.com/go/channel v1.11.0/go.mod h1:IdtI0uWGqhEeatSB62VOoJ8FSUhJ9/+iGkJVqp74CGE= cloud.google.com/go/channel v1.12.0/go.mod h1:VkxCGKASi4Cq7TbXxlaBezonAYpp1GCnKMY6tnMQnLU= cloud.google.com/go/cloudbuild v1.3.0/go.mod h1:WequR4ULxlqvMsjDEEEFnOG5ZSRSgWOywXYDb1vPE6U= cloud.google.com/go/cloudbuild v1.4.0/go.mod h1:5Qwa40LHiOXmz3386FrjrYM93rM/hdRr7b53sySrTqA= cloud.google.com/go/cloudbuild v1.6.0/go.mod h1:UIbc/w9QCbH12xX+ezUsgblrWv+Cv4Tw83GiSMHOn9M= cloud.google.com/go/cloudbuild v1.7.0/go.mod h1:zb5tWh2XI6lR9zQmsm1VRA+7OCuve5d8S+zJUul8KTg= cloud.google.com/go/cloudbuild v1.9.0/go.mod h1:qK1d7s4QlO0VwfYn5YuClDGg2hfmLZEb4wQGAbIgL1s= cloud.google.com/go/clouddms v1.3.0/go.mod h1:oK6XsCDdW4Ib3jCCBugx+gVjevp2TMXFtgxvPSee3OM= cloud.google.com/go/clouddms v1.4.0/go.mod h1:Eh7sUGCC+aKry14O1NRljhjyrr0NFC0G2cjwX0cByRk= cloud.google.com/go/clouddms v1.5.0/go.mod h1:QSxQnhikCLUw13iAbffF2CZxAER3xDGNHjsTAkQJcQA= cloud.google.com/go/cloudtasks v1.5.0/go.mod h1:fD92REy1x5woxkKEkLdvavGnPJGEn8Uic9nWuLzqCpY= cloud.google.com/go/cloudtasks v1.6.0/go.mod h1:C6Io+sxuke9/KNRkbQpihnW93SWDU3uXt92nu85HkYI= cloud.google.com/go/cloudtasks v1.7.0/go.mod h1:ImsfdYWwlWNJbdgPIIGJWC+gemEGTBK/SunNQQNCAb4= cloud.google.com/go/cloudtasks v1.8.0/go.mod h1:gQXUIwCSOI4yPVK7DgTVFiiP0ZW/eQkydWzwVMdHxrI= cloud.google.com/go/cloudtasks v1.9.0/go.mod h1:w+EyLsVkLWHcOaqNEyvcKAsWp9p29dL6uL9Nst1cI7Y= cloud.google.com/go/cloudtasks v1.10.0/go.mod h1:NDSoTLkZ3+vExFEWu2UJV1arUyzVDAiZtdWcsUyNwBs= cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= cloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU= cloud.google.com/go/compute v1.12.0/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= cloud.google.com/go/compute v1.13.0/go.mod h1:5aPTS0cUNMIc1CE546K+Th6weJUNQErARyZtRXDJ8GE= cloud.google.com/go/compute v1.14.0/go.mod h1:YfLtxrj9sU4Yxv+sXzZkyPjEyPBZfXHUvjxega5vAdo= cloud.google.com/go/compute v1.15.1/go.mod h1:bjjoF/NtFUrkD/urWfdHaKuOPDR5nWIs63rR+SXhcpA= cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs= cloud.google.com/go/compute v1.19.0/go.mod h1:rikpw2y+UMidAe9tISo04EHNOIf42RLYF/q8Bs93scU= cloud.google.com/go/compute/metadata v0.1.0/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZEXYonfTBHHFPO/4UU= cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU= cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= cloud.google.com/go/contactcenterinsights v1.3.0/go.mod h1:Eu2oemoePuEFc/xKFPjbTuPSj0fYJcPls9TFlPNnHHY= cloud.google.com/go/contactcenterinsights v1.4.0/go.mod h1:L2YzkGbPsv+vMQMCADxJoT9YiTTnSEd6fEvCeHTYVck= cloud.google.com/go/contactcenterinsights v1.6.0/go.mod h1:IIDlT6CLcDoyv79kDv8iWxMSTZhLxSCofVV5W6YFM/w= cloud.google.com/go/container v1.6.0/go.mod h1:Xazp7GjJSeUYo688S+6J5V+n/t+G5sKBTFkKNudGRxg= cloud.google.com/go/container v1.7.0/go.mod h1:Dp5AHtmothHGX3DwwIHPgq45Y8KmNsgN3amoYfxVkLo= cloud.google.com/go/container v1.13.1/go.mod h1:6wgbMPeQRw9rSnKBCAJXnds3Pzj03C4JHamr8asWKy4= cloud.google.com/go/container v1.14.0/go.mod h1:3AoJMPhHfLDxLvrlVWaK57IXzaPnLaZq63WX59aQBfM= cloud.google.com/go/container v1.15.0/go.mod h1:ft+9S0WGjAyjDggg5S06DXj+fHJICWg8L7isCQe9pQA= cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I= cloud.google.com/go/containeranalysis v0.6.0/go.mod h1:HEJoiEIu+lEXM+k7+qLCci0h33lX3ZqoYFdmPcoO7s4= cloud.google.com/go/containeranalysis v0.7.0/go.mod h1:9aUL+/vZ55P2CXfuZjS4UjQ9AgXoSw8Ts6lemfmxBxI= cloud.google.com/go/containeranalysis v0.9.0/go.mod h1:orbOANbwk5Ejoom+s+DUCTTJ7IBdBQJDcSylAx/on9s= cloud.google.com/go/datacatalog v1.3.0/go.mod h1:g9svFY6tuR+j+hrTw3J2dNcmI0dzmSiyOzm8kpLq0a0= cloud.google.com/go/datacatalog v1.5.0/go.mod h1:M7GPLNQeLfWqeIm3iuiruhPzkt65+Bx8dAKvScX8jvs= cloud.google.com/go/datacatalog v1.6.0/go.mod h1:+aEyF8JKg+uXcIdAmmaMUmZ3q1b/lKLtXCmXdnc0lbc= cloud.google.com/go/datacatalog v1.7.0/go.mod h1:9mEl4AuDYWw81UGc41HonIHH7/sn52H0/tc8f8ZbZIE= cloud.google.com/go/datacatalog v1.8.0/go.mod h1:KYuoVOv9BM8EYz/4eMFxrr4DUKhGIOXxZoKYF5wdISM= cloud.google.com/go/datacatalog v1.8.1/go.mod h1:RJ58z4rMp3gvETA465Vg+ag8BGgBdnRPEMMSTr5Uv+M= cloud.google.com/go/datacatalog v1.12.0/go.mod h1:CWae8rFkfp6LzLumKOnmVh4+Zle4A3NXLzVJ1d1mRm0= cloud.google.com/go/datacatalog v1.13.0/go.mod h1:E4Rj9a5ZtAxcQJlEBTLgMTphfP11/lNaAshpoBgemX8= cloud.google.com/go/dataflow v0.6.0/go.mod h1:9QwV89cGoxjjSR9/r7eFDqqjtvbKxAK2BaYU6PVk9UM= cloud.google.com/go/dataflow v0.7.0/go.mod h1:PX526vb4ijFMesO1o202EaUmouZKBpjHsTlCtB4parQ= cloud.google.com/go/dataflow v0.8.0/go.mod h1:Rcf5YgTKPtQyYz8bLYhFoIV/vP39eL7fWNcSOyFfLJE= cloud.google.com/go/dataform v0.3.0/go.mod h1:cj8uNliRlHpa6L3yVhDOBrUXH+BPAO1+KFMQQNSThKo= cloud.google.com/go/dataform v0.4.0/go.mod h1:fwV6Y4Ty2yIFL89huYlEkwUPtS7YZinZbzzj5S9FzCE= cloud.google.com/go/dataform v0.5.0/go.mod h1:GFUYRe8IBa2hcomWplodVmUx/iTL0FrsauObOM3Ipr0= cloud.google.com/go/dataform v0.6.0/go.mod h1:QPflImQy33e29VuapFdf19oPbE4aYTJxr31OAPV+ulA= cloud.google.com/go/dataform v0.7.0/go.mod h1:7NulqnVozfHvWUBpMDfKMUESr+85aJsC/2O0o3jWPDE= cloud.google.com/go/datafusion v1.4.0/go.mod h1:1Zb6VN+W6ALo85cXnM1IKiPw+yQMKMhB9TsTSRDo/38= cloud.google.com/go/datafusion v1.5.0/go.mod h1:Kz+l1FGHB0J+4XF2fud96WMmRiq/wj8N9u007vyXZ2w= cloud.google.com/go/datafusion v1.6.0/go.mod h1:WBsMF8F1RhSXvVM8rCV3AeyWVxcC2xY6vith3iw3S+8= cloud.google.com/go/datalabeling v0.5.0/go.mod h1:TGcJ0G2NzcsXSE/97yWjIZO0bXj0KbVlINXMG9ud42I= cloud.google.com/go/datalabeling v0.6.0/go.mod h1:WqdISuk/+WIGeMkpw/1q7bK/tFEZxsrFJOJdY2bXvTQ= cloud.google.com/go/datalabeling v0.7.0/go.mod h1:WPQb1y08RJbmpM3ww0CSUAGweL0SxByuW2E+FU+wXcM= cloud.google.com/go/dataplex v1.3.0/go.mod h1:hQuRtDg+fCiFgC8j0zV222HvzFQdRd+SVX8gdmFcZzA= cloud.google.com/go/dataplex v1.4.0/go.mod h1:X51GfLXEMVJ6UN47ESVqvlsRplbLhcsAt0kZCCKsU0A= cloud.google.com/go/dataplex v1.5.2/go.mod h1:cVMgQHsmfRoI5KFYq4JtIBEUbYwc3c7tXmIDhRmNNVQ= cloud.google.com/go/dataplex v1.6.0/go.mod h1:bMsomC/aEJOSpHXdFKFGQ1b0TDPIeL28nJObeO1ppRs= cloud.google.com/go/dataproc v1.7.0/go.mod h1:CKAlMjII9H90RXaMpSxQ8EU6dQx6iAYNPcYPOkSbi8s= cloud.google.com/go/dataproc v1.8.0/go.mod h1:5OW+zNAH0pMpw14JVrPONsxMQYMBqJuzORhIBfBn9uI= cloud.google.com/go/dataproc v1.12.0/go.mod h1:zrF3aX0uV3ikkMz6z4uBbIKyhRITnxvr4i3IjKsKrw4= cloud.google.com/go/dataqna v0.5.0/go.mod h1:90Hyk596ft3zUQ8NkFfvICSIfHFh1Bc7C4cK3vbhkeo= cloud.google.com/go/dataqna v0.6.0/go.mod h1:1lqNpM7rqNLVgWBJyk5NF6Uen2PHym0jtVJonplVsDA= cloud.google.com/go/dataqna v0.7.0/go.mod h1:Lx9OcIIeqCrw1a6KdO3/5KMP1wAmTc0slZWwP12Qq3c= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/datastore v1.10.0/go.mod h1:PC5UzAmDEkAmkfaknstTYbNpgE49HAgW2J1gcgUfmdM= cloud.google.com/go/datastore v1.11.0/go.mod h1:TvGxBIHCS50u8jzG+AW/ppf87v1of8nwzFNgEZU1D3c= cloud.google.com/go/datastream v1.2.0/go.mod h1:i/uTP8/fZwgATHS/XFu0TcNUhuA0twZxxQ3EyCUQMwo= cloud.google.com/go/datastream v1.3.0/go.mod h1:cqlOX8xlyYF/uxhiKn6Hbv6WjwPPuI9W2M9SAXwaLLQ= cloud.google.com/go/datastream v1.4.0/go.mod h1:h9dpzScPhDTs5noEMQVWP8Wx8AFBRyS0s8KWPx/9r0g= cloud.google.com/go/datastream v1.5.0/go.mod h1:6TZMMNPwjUqZHBKPQ1wwXpb0d5VDVPl2/XoS5yi88q4= cloud.google.com/go/datastream v1.6.0/go.mod h1:6LQSuswqLa7S4rPAOZFVjHIG3wJIjZcZrw8JDEDJuIs= cloud.google.com/go/datastream v1.7.0/go.mod h1:uxVRMm2elUSPuh65IbZpzJNMbuzkcvu5CjMqVIUHrww= cloud.google.com/go/deploy v1.4.0/go.mod h1:5Xghikd4VrmMLNaF6FiRFDlHb59VM59YoDQnOUdsH/c= cloud.google.com/go/deploy v1.5.0/go.mod h1:ffgdD0B89tToyW/U/D2eL0jN2+IEV/3EMuXHA0l4r+s= cloud.google.com/go/deploy v1.6.0/go.mod h1:f9PTHehG/DjCom3QH0cntOVRm93uGBDt2vKzAPwpXQI= cloud.google.com/go/deploy v1.8.0/go.mod h1:z3myEJnA/2wnB4sgjqdMfgxCA0EqC3RBTNcVPs93mtQ= cloud.google.com/go/dialogflow v1.15.0/go.mod h1:HbHDWs33WOGJgn6rfzBW1Kv807BE3O1+xGbn59zZWI4= cloud.google.com/go/dialogflow v1.16.1/go.mod h1:po6LlzGfK+smoSmTBnbkIZY2w8ffjz/RcGSS+sh1el0= cloud.google.com/go/dialogflow v1.17.0/go.mod h1:YNP09C/kXA1aZdBgC/VtXX74G/TKn7XVCcVumTflA+8= cloud.google.com/go/dialogflow v1.18.0/go.mod h1:trO7Zu5YdyEuR+BhSNOqJezyFQ3aUzz0njv7sMx/iek= cloud.google.com/go/dialogflow v1.19.0/go.mod h1:JVmlG1TwykZDtxtTXujec4tQ+D8SBFMoosgy+6Gn0s0= cloud.google.com/go/dialogflow v1.29.0/go.mod h1:b+2bzMe+k1s9V+F2jbJwpHPzrnIyHihAdRFMtn2WXuM= cloud.google.com/go/dialogflow v1.31.0/go.mod h1:cuoUccuL1Z+HADhyIA7dci3N5zUssgpBJmCzI6fNRB4= cloud.google.com/go/dialogflow v1.32.0/go.mod h1:jG9TRJl8CKrDhMEcvfcfFkkpp8ZhgPz3sBGmAUYJ2qE= cloud.google.com/go/dlp v1.6.0/go.mod h1:9eyB2xIhpU0sVwUixfBubDoRwP+GjeUoxxeueZmqvmM= cloud.google.com/go/dlp v1.7.0/go.mod h1:68ak9vCiMBjbasxeVD17hVPxDEck+ExiHavX8kiHG+Q= cloud.google.com/go/dlp v1.9.0/go.mod h1:qdgmqgTyReTz5/YNSSuueR8pl7hO0o9bQ39ZhtgkWp4= cloud.google.com/go/documentai v1.7.0/go.mod h1:lJvftZB5NRiFSX4moiye1SMxHx0Bc3x1+p9e/RfXYiU= cloud.google.com/go/documentai v1.8.0/go.mod h1:xGHNEB7CtsnySCNrCFdCyyMz44RhFEEX2Q7UD0c5IhU= cloud.google.com/go/documentai v1.9.0/go.mod h1:FS5485S8R00U10GhgBC0aNGrJxBP8ZVpEeJ7PQDZd6k= cloud.google.com/go/documentai v1.10.0/go.mod h1:vod47hKQIPeCfN2QS/jULIvQTugbmdc0ZvxxfQY1bg4= cloud.google.com/go/documentai v1.16.0/go.mod h1:o0o0DLTEZ+YnJZ+J4wNfTxmDVyrkzFvttBXXtYRMHkM= cloud.google.com/go/documentai v1.18.0/go.mod h1:F6CK6iUH8J81FehpskRmhLq/3VlwQvb7TvwOceQ2tbs= cloud.google.com/go/domains v0.6.0/go.mod h1:T9Rz3GasrpYk6mEGHh4rymIhjlnIuB4ofT1wTxDeT4Y= cloud.google.com/go/domains v0.7.0/go.mod h1:PtZeqS1xjnXuRPKE/88Iru/LdfoRyEHYA9nFQf4UKpg= cloud.google.com/go/domains v0.8.0/go.mod h1:M9i3MMDzGFXsydri9/vW+EWz9sWb4I6WyHqdlAk0idE= cloud.google.com/go/edgecontainer v0.1.0/go.mod h1:WgkZ9tp10bFxqO8BLPqv2LlfmQF1X8lZqwW4r1BTajk= cloud.google.com/go/edgecontainer v0.2.0/go.mod h1:RTmLijy+lGpQ7BXuTDa4C4ssxyXT34NIuHIgKuP4s5w= cloud.google.com/go/edgecontainer v0.3.0/go.mod h1:FLDpP4nykgwwIfcLt6zInhprzw0lEi2P1fjO6Ie0qbc= cloud.google.com/go/edgecontainer v1.0.0/go.mod h1:cttArqZpBB2q58W/upSG++ooo6EsblxDIolxa3jSjbY= cloud.google.com/go/errorreporting v0.3.0/go.mod h1:xsP2yaAp+OAW4OIm60An2bbLpqIhKXdWR/tawvl7QzU= cloud.google.com/go/essentialcontacts v1.3.0/go.mod h1:r+OnHa5jfj90qIfZDO/VztSFqbQan7HV75p8sA+mdGI= cloud.google.com/go/essentialcontacts v1.4.0/go.mod h1:8tRldvHYsmnBCHdFpvU+GL75oWiBKl80BiqlFh9tp+8= cloud.google.com/go/essentialcontacts v1.5.0/go.mod h1:ay29Z4zODTuwliK7SnX8E86aUF2CTzdNtvv42niCX0M= cloud.google.com/go/eventarc v1.7.0/go.mod h1:6ctpF3zTnaQCxUjHUdcfgcA1A2T309+omHZth7gDfmc= cloud.google.com/go/eventarc v1.8.0/go.mod h1:imbzxkyAU4ubfsaKYdQg04WS1NvncblHEup4kvF+4gw= cloud.google.com/go/eventarc v1.10.0/go.mod h1:u3R35tmZ9HvswGRBnF48IlYgYeBcPUCjkr4BTdem2Kw= cloud.google.com/go/eventarc v1.11.0/go.mod h1:PyUjsUKPWoRBCHeOxZd/lbOOjahV41icXyUY5kSTvVY= cloud.google.com/go/filestore v1.3.0/go.mod h1:+qbvHGvXU1HaKX2nD0WEPo92TP/8AQuCVEBXNY9z0+w= cloud.google.com/go/filestore v1.4.0/go.mod h1:PaG5oDfo9r224f8OYXURtAsY+Fbyq/bLYoINEK8XQAI= cloud.google.com/go/filestore v1.5.0/go.mod h1:FqBXDWBp4YLHqRnVGveOkHDf8svj9r5+mUDLupOWEDs= cloud.google.com/go/filestore v1.6.0/go.mod h1:di5unNuss/qfZTw2U9nhFqo8/ZDSc466dre85Kydllg= cloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE= cloud.google.com/go/functions v1.6.0/go.mod h1:3H1UA3qiIPRWD7PeZKLvHZ9SaQhR26XIJcC0A5GbvAk= cloud.google.com/go/functions v1.7.0/go.mod h1:+d+QBcWM+RsrgZfV9xo6KfA1GlzJfxcfZcRPEhDDfzg= cloud.google.com/go/functions v1.8.0/go.mod h1:RTZ4/HsQjIqIYP9a9YPbU+QFoQsAlYgrwOXJWHn1POY= cloud.google.com/go/functions v1.9.0/go.mod h1:Y+Dz8yGguzO3PpIjhLTbnqV1CWmgQ5UwtlpzoyquQ08= cloud.google.com/go/functions v1.10.0/go.mod h1:0D3hEOe3DbEvCXtYOZHQZmD+SzYsi1YbI7dGvHfldXw= cloud.google.com/go/functions v1.12.0/go.mod h1:AXWGrF3e2C/5ehvwYo/GH6O5s09tOPksiKhz+hH8WkA= cloud.google.com/go/functions v1.13.0/go.mod h1:EU4O007sQm6Ef/PwRsI8N2umygGqPBS/IZQKBQBcJ3c= cloud.google.com/go/gaming v1.5.0/go.mod h1:ol7rGcxP/qHTRQE/RO4bxkXq+Fix0j6D4LFPzYTIrDM= cloud.google.com/go/gaming v1.6.0/go.mod h1:YMU1GEvA39Qt3zWGyAVA9bpYz/yAhTvaQ1t2sK4KPUA= cloud.google.com/go/gaming v1.7.0/go.mod h1:LrB8U7MHdGgFG851iHAfqUdLcKBdQ55hzXy9xBJz0+w= cloud.google.com/go/gaming v1.8.0/go.mod h1:xAqjS8b7jAVW0KFYeRUxngo9My3f33kFmua++Pi+ggM= cloud.google.com/go/gaming v1.9.0/go.mod h1:Fc7kEmCObylSWLO334NcO+O9QMDyz+TKC4v1D7X+Bc0= cloud.google.com/go/gkebackup v0.2.0/go.mod h1:XKvv/4LfG829/B8B7xRkk8zRrOEbKtEam6yNfuQNH60= cloud.google.com/go/gkebackup v0.3.0/go.mod h1:n/E671i1aOQvUxT541aTkCwExO/bTer2HDlj4TsBRAo= cloud.google.com/go/gkebackup v0.4.0/go.mod h1:byAyBGUwYGEEww7xsbnUTBHIYcOPy/PgUWUtOeRm9Vg= cloud.google.com/go/gkeconnect v0.5.0/go.mod h1:c5lsNAg5EwAy7fkqX/+goqFsU1Da/jQFqArp+wGNr/o= cloud.google.com/go/gkeconnect v0.6.0/go.mod h1:Mln67KyU/sHJEBY8kFZ0xTeyPtzbq9StAVvEULYK16A= cloud.google.com/go/gkeconnect v0.7.0/go.mod h1:SNfmVqPkaEi3bF/B3CNZOAYPYdg7sU+obZ+QTky2Myw= cloud.google.com/go/gkehub v0.9.0/go.mod h1:WYHN6WG8w9bXU0hqNxt8rm5uxnk8IH+lPY9J2TV7BK0= cloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y977wO+hBH0= cloud.google.com/go/gkehub v0.11.0/go.mod h1:JOWHlmN+GHyIbuWQPl47/C2RFhnFKH38jH9Ascu3n0E= cloud.google.com/go/gkehub v0.12.0/go.mod h1:djiIwwzTTBrF5NaXCGv3mf7klpEMcST17VBTVVDcuaw= cloud.google.com/go/gkemulticloud v0.3.0/go.mod h1:7orzy7O0S+5kq95e4Hpn7RysVA7dPs8W/GgfUtsPbrA= cloud.google.com/go/gkemulticloud v0.4.0/go.mod h1:E9gxVBnseLWCk24ch+P9+B2CoDFJZTyIgLKSalC7tuI= cloud.google.com/go/gkemulticloud v0.5.0/go.mod h1:W0JDkiyi3Tqh0TJr//y19wyb1yf8llHVto2Htf2Ja3Y= cloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc= cloud.google.com/go/gsuiteaddons v1.3.0/go.mod h1:EUNK/J1lZEZO8yPtykKxLXI6JSVN2rg9bN8SXOa0bgM= cloud.google.com/go/gsuiteaddons v1.4.0/go.mod h1:rZK5I8hht7u7HxFQcFei0+AtfS9uSushomRlg+3ua1o= cloud.google.com/go/gsuiteaddons v1.5.0/go.mod h1:TFCClYLd64Eaa12sFVmUyG62tk4mdIsI7pAnSXRkcFo= cloud.google.com/go/iam v0.1.0/go.mod h1:vcUNEa0pEm0qRVpmWepWaFMIAI8/hjB9mO8rNCJtF6c= cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= cloud.google.com/go/iam v0.5.0/go.mod h1:wPU9Vt0P4UmCux7mqtRu6jcpPAb74cP1fh50J3QpkUc= cloud.google.com/go/iam v0.6.0/go.mod h1:+1AH33ueBne5MzYccyMHtEKqLE4/kJOibtffMHDMFMc= cloud.google.com/go/iam v0.7.0/go.mod h1:H5Br8wRaDGNc8XP3keLc4unfUUZeyH3Sfl9XpQEYOeg= cloud.google.com/go/iam v0.8.0/go.mod h1:lga0/y3iH6CX7sYqypWJ33hf7kkfXJag67naqGESjkE= cloud.google.com/go/iam v0.11.0/go.mod h1:9PiLDanza5D+oWFZiH1uG+RnRCfEGKoyl6yo4cgWZGY= cloud.google.com/go/iam v0.12.0/go.mod h1:knyHGviacl11zrtZUoDuYpDgLjvr28sLQaG0YB2GYAY= cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0= cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8= cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE= cloud.google.com/go/iap v1.4.0/go.mod h1:RGFwRJdihTINIe4wZ2iCP0zF/qu18ZwyKxrhMhygBEc= cloud.google.com/go/iap v1.5.0/go.mod h1:UH/CGgKd4KyohZL5Pt0jSKE4m3FR51qg6FKQ/z/Ix9A= cloud.google.com/go/iap v1.6.0/go.mod h1:NSuvI9C/j7UdjGjIde7t7HBz+QTwBcapPE07+sSRcLk= cloud.google.com/go/iap v1.7.0/go.mod h1:beqQx56T9O1G1yNPph+spKpNibDlYIiIixiqsQXxLIo= cloud.google.com/go/iap v1.7.1/go.mod h1:WapEwPc7ZxGt2jFGB/C/bm+hP0Y6NXzOYGjpPnmMS74= cloud.google.com/go/ids v1.1.0/go.mod h1:WIuwCaYVOzHIj2OhN9HAwvW+DBdmUAdcWlFxRl+KubM= cloud.google.com/go/ids v1.2.0/go.mod h1:5WXvp4n25S0rA/mQWAg1YEEBBq6/s+7ml1RDCW1IrcY= cloud.google.com/go/ids v1.3.0/go.mod h1:JBdTYwANikFKaDP6LtW5JAi4gubs57SVNQjemdt6xV4= cloud.google.com/go/iot v1.3.0/go.mod h1:r7RGh2B61+B8oz0AGE+J72AhA0G7tdXItODWsaA2oLs= cloud.google.com/go/iot v1.4.0/go.mod h1:dIDxPOn0UvNDUMD8Ger7FIaTuvMkj+aGk94RPP0iV+g= cloud.google.com/go/iot v1.5.0/go.mod h1:mpz5259PDl3XJthEmh9+ap0affn/MqNSP4My77Qql9o= cloud.google.com/go/iot v1.6.0/go.mod h1:IqdAsmE2cTYYNO1Fvjfzo9po179rAtJeVGUvkLN3rLE= cloud.google.com/go/kms v1.4.0/go.mod h1:fajBHndQ+6ubNw6Ss2sSd+SWvjL26RNo/dr7uxsnnOA= cloud.google.com/go/kms v1.5.0/go.mod h1:QJS2YY0eJGBg3mnDfuaCyLauWwBJiHRboYxJ++1xJNg= cloud.google.com/go/kms v1.6.0/go.mod h1:Jjy850yySiasBUDi6KFUwUv2n1+o7QZFyuUJg6OgjA0= cloud.google.com/go/kms v1.8.0/go.mod h1:4xFEhYFqvW+4VMELtZyxomGSYtSQKzM178ylFW4jMAg= cloud.google.com/go/kms v1.9.0/go.mod h1:qb1tPTgfF9RQP8e1wq4cLFErVuTJv7UsSC915J8dh3w= cloud.google.com/go/kms v1.10.0/go.mod h1:ng3KTUtQQU9bPX3+QGLsflZIHlkbn8amFAMY63m8d24= cloud.google.com/go/kms v1.10.1/go.mod h1:rIWk/TryCkR59GMC3YtHtXeLzd634lBbKenvyySAyYI= cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic= cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI= cloud.google.com/go/language v1.7.0/go.mod h1:DJ6dYN/W+SQOjF8e1hLQXMF21AkH2w9wiPzPCJa2MIE= cloud.google.com/go/language v1.8.0/go.mod h1:qYPVHf7SPoNNiCL2Dr0FfEFNil1qi3pQEyygwpgVKB8= cloud.google.com/go/language v1.9.0/go.mod h1:Ns15WooPM5Ad/5no/0n81yUetis74g3zrbeJBE+ptUY= cloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8= cloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6t/iPhY2Tyfu08= cloud.google.com/go/lifesciences v0.8.0/go.mod h1:lFxiEOMqII6XggGbOnKiyZ7IBwoIqA84ClvoezaA/bo= cloud.google.com/go/logging v1.6.1/go.mod h1:5ZO0mHHbvm8gEmeEUHrmDlTDSu5imF6MUP9OfilNXBw= cloud.google.com/go/logging v1.7.0/go.mod h1:3xjP2CjkM3ZkO73aj4ASA5wRPGGCRrPIAeNqVNkzY8M= cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc= cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA= cloud.google.com/go/longrunning v0.1.1/go.mod h1:UUFxuDWkv22EuY93jjmDMFT5GPQKeFVJBIF6QlTqdsE= cloud.google.com/go/longrunning v0.3.0/go.mod h1:qth9Y41RRSUE69rDcOn6DdK3HfQfsUI0YSmW3iIlLJc= cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo= cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE= cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY= cloud.google.com/go/managedidentities v1.3.0/go.mod h1:UzlW3cBOiPrzucO5qWkNkh0w33KFtBJU281hacNvsdE= cloud.google.com/go/managedidentities v1.4.0/go.mod h1:NWSBYbEMgqmbZsLIyKvxrYbtqOsxY1ZrGM+9RgDqInM= cloud.google.com/go/managedidentities v1.5.0/go.mod h1:+dWcZ0JlUmpuxpIDfyP5pP5y0bLdRwOS4Lp7gMni/LA= cloud.google.com/go/maps v0.1.0/go.mod h1:BQM97WGyfw9FWEmQMpZ5T6cpovXXSd1cGmFma94eubI= cloud.google.com/go/maps v0.6.0/go.mod h1:o6DAMMfb+aINHz/p/jbcY+mYeXBoZoxTfdSQ8VAJaCw= cloud.google.com/go/maps v0.7.0/go.mod h1:3GnvVl3cqeSvgMcpRlQidXsPYuDGQ8naBis7MVzpXsY= cloud.google.com/go/mediatranslation v0.5.0/go.mod h1:jGPUhGTybqsPQn91pNXw0xVHfuJ3leR1wj37oU3y1f4= cloud.google.com/go/mediatranslation v0.6.0/go.mod h1:hHdBCTYNigsBxshbznuIMFNe5QXEowAuNmmC7h8pu5w= cloud.google.com/go/mediatranslation v0.7.0/go.mod h1:LCnB/gZr90ONOIQLgSXagp8XUW1ODs2UmUMvcgMfI2I= cloud.google.com/go/memcache v1.4.0/go.mod h1:rTOfiGZtJX1AaFUrOgsMHX5kAzaTQ8azHiuDoTPzNsE= cloud.google.com/go/memcache v1.5.0/go.mod h1:dk3fCK7dVo0cUU2c36jKb4VqKPS22BTkf81Xq617aWM= cloud.google.com/go/memcache v1.6.0/go.mod h1:XS5xB0eQZdHtTuTF9Hf8eJkKtR3pVRCcvJwtm68T3rA= cloud.google.com/go/memcache v1.7.0/go.mod h1:ywMKfjWhNtkQTxrWxCkCFkoPjLHPW6A7WOTVI8xy3LY= cloud.google.com/go/memcache v1.9.0/go.mod h1:8oEyzXCu+zo9RzlEaEjHl4KkgjlNDaXbCQeQWlzNFJM= cloud.google.com/go/metastore v1.5.0/go.mod h1:2ZNrDcQwghfdtCwJ33nM0+GrBGlVuh8rakL3vdPY3XY= cloud.google.com/go/metastore v1.6.0/go.mod h1:6cyQTls8CWXzk45G55x57DVQ9gWg7RiH65+YgPsNh9s= cloud.google.com/go/metastore v1.7.0/go.mod h1:s45D0B4IlsINu87/AsWiEVYbLaIMeUSoxlKKDqBGFS8= cloud.google.com/go/metastore v1.8.0/go.mod h1:zHiMc4ZUpBiM7twCIFQmJ9JMEkDSyZS9U12uf7wHqSI= cloud.google.com/go/metastore v1.10.0/go.mod h1:fPEnH3g4JJAk+gMRnrAnoqyv2lpUCqJPWOodSaf45Eo= cloud.google.com/go/monitoring v1.7.0/go.mod h1:HpYse6kkGo//7p6sT0wsIC6IBDET0RhIsnmlA53dvEk= cloud.google.com/go/monitoring v1.8.0/go.mod h1:E7PtoMJ1kQXWxPjB6mv2fhC5/15jInuulFdYYtlcvT4= cloud.google.com/go/monitoring v1.12.0/go.mod h1:yx8Jj2fZNEkL/GYZyTLS4ZtZEZN8WtDEiEqG4kLK50w= cloud.google.com/go/monitoring v1.13.0/go.mod h1:k2yMBAB1H9JT/QETjNkgdCGD9bPF712XiLTVr+cBrpw= cloud.google.com/go/monitoring v1.24.2 h1:5OTsoJ1dXYIiMiuL+sYscLc9BumrL3CarVLL7dd7lHM= cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U= cloud.google.com/go/networkconnectivity v1.4.0/go.mod h1:nOl7YL8odKyAOtzNX73/M5/mGZgqqMeryi6UPZTk/rA= cloud.google.com/go/networkconnectivity v1.5.0/go.mod h1:3GzqJx7uhtlM3kln0+x5wyFvuVH1pIBJjhCpjzSt75o= cloud.google.com/go/networkconnectivity v1.6.0/go.mod h1:OJOoEXW+0LAxHh89nXd64uGG+FbQoeH8DtxCHVOMlaM= cloud.google.com/go/networkconnectivity v1.7.0/go.mod h1:RMuSbkdbPwNMQjB5HBWD5MpTBnNm39iAVpC3TmsExt8= cloud.google.com/go/networkconnectivity v1.10.0/go.mod h1:UP4O4sWXJG13AqrTdQCD9TnLGEbtNRqjuaaA7bNjF5E= cloud.google.com/go/networkconnectivity v1.11.0/go.mod h1:iWmDD4QF16VCDLXUqvyspJjIEtBR/4zq5hwnY2X3scM= cloud.google.com/go/networkmanagement v1.4.0/go.mod h1:Q9mdLLRn60AsOrPc8rs8iNV6OHXaGcDdsIQe1ohekq8= cloud.google.com/go/networkmanagement v1.5.0/go.mod h1:ZnOeZ/evzUdUsnvRt792H0uYEnHQEMaz+REhhzJRcf4= cloud.google.com/go/networkmanagement v1.6.0/go.mod h1:5pKPqyXjB/sgtvB5xqOemumoQNB7y95Q7S+4rjSOPYY= cloud.google.com/go/networksecurity v0.5.0/go.mod h1:xS6fOCoqpVC5zx15Z/MqkfDwH4+m/61A3ODiDV1xmiQ= cloud.google.com/go/networksecurity v0.6.0/go.mod h1:Q5fjhTr9WMI5mbpRYEbiexTzROf7ZbDzvzCrNl14nyU= cloud.google.com/go/networksecurity v0.7.0/go.mod h1:mAnzoxx/8TBSyXEeESMy9OOYwo1v+gZ5eMRnsT5bC8k= cloud.google.com/go/networksecurity v0.8.0/go.mod h1:B78DkqsxFG5zRSVuwYFRZ9Xz8IcQ5iECsNrPn74hKHU= cloud.google.com/go/notebooks v1.2.0/go.mod h1:9+wtppMfVPUeJ8fIWPOq1UnATHISkGXGqTkxeieQ6UY= cloud.google.com/go/notebooks v1.3.0/go.mod h1:bFR5lj07DtCPC7YAAJ//vHskFBxA5JzYlH68kXVdk34= cloud.google.com/go/notebooks v1.4.0/go.mod h1:4QPMngcwmgb6uw7Po99B2xv5ufVoIQ7nOGDyL4P8AgA= cloud.google.com/go/notebooks v1.5.0/go.mod h1:q8mwhnP9aR8Hpfnrc5iN5IBhrXUy8S2vuYs+kBJ/gu0= cloud.google.com/go/notebooks v1.7.0/go.mod h1:PVlaDGfJgj1fl1S3dUwhFMXFgfYGhYQt2164xOMONmE= cloud.google.com/go/notebooks v1.8.0/go.mod h1:Lq6dYKOYOWUCTvw5t2q1gp1lAp0zxAxRycayS0iJcqQ= cloud.google.com/go/optimization v1.1.0/go.mod h1:5po+wfvX5AQlPznyVEZjGJTMr4+CAkJf2XSTQOOl9l4= cloud.google.com/go/optimization v1.2.0/go.mod h1:Lr7SOHdRDENsh+WXVmQhQTrzdu9ybg0NecjHidBq6xs= cloud.google.com/go/optimization v1.3.1/go.mod h1:IvUSefKiwd1a5p0RgHDbWCIbDFgKuEdB+fPPuP0IDLI= cloud.google.com/go/orchestration v1.3.0/go.mod h1:Sj5tq/JpWiB//X/q3Ngwdl5K7B7Y0KZ7bfv0wL6fqVA= cloud.google.com/go/orchestration v1.4.0/go.mod h1:6W5NLFWs2TlniBphAViZEVhrXRSMgUGDfW7vrWKvsBk= cloud.google.com/go/orchestration v1.6.0/go.mod h1:M62Bevp7pkxStDfFfTuCOaXgaaqRAga1yKyoMtEoWPQ= cloud.google.com/go/orgpolicy v1.4.0/go.mod h1:xrSLIV4RePWmP9P3tBl8S93lTmlAxjm06NSm2UTmKvE= cloud.google.com/go/orgpolicy v1.5.0/go.mod h1:hZEc5q3wzwXJaKrsx5+Ewg0u1LxJ51nNFlext7Tanwc= cloud.google.com/go/orgpolicy v1.10.0/go.mod h1:w1fo8b7rRqlXlIJbVhOMPrwVljyuW5mqssvBtU18ONc= cloud.google.com/go/osconfig v1.7.0/go.mod h1:oVHeCeZELfJP7XLxcBGTMBvRO+1nQ5tFG9VQTmYS2Fs= cloud.google.com/go/osconfig v1.8.0/go.mod h1:EQqZLu5w5XA7eKizepumcvWx+m8mJUhEwiPqWiZeEdg= cloud.google.com/go/osconfig v1.9.0/go.mod h1:Yx+IeIZJ3bdWmzbQU4fxNl8xsZ4amB+dygAwFPlvnNo= cloud.google.com/go/osconfig v1.10.0/go.mod h1:uMhCzqC5I8zfD9zDEAfvgVhDS8oIjySWh+l4WK6GnWw= cloud.google.com/go/osconfig v1.11.0/go.mod h1:aDICxrur2ogRd9zY5ytBLV89KEgT2MKB2L/n6x1ooPw= cloud.google.com/go/oslogin v1.4.0/go.mod h1:YdgMXWRaElXz/lDk1Na6Fh5orF7gvmJ0FGLIs9LId4E= cloud.google.com/go/oslogin v1.5.0/go.mod h1:D260Qj11W2qx/HVF29zBg+0fd6YCSjSqLUkY/qEenQU= cloud.google.com/go/oslogin v1.6.0/go.mod h1:zOJ1O3+dTU8WPlGEkFSh7qeHPPSoxrcMbbK1Nm2iX70= cloud.google.com/go/oslogin v1.7.0/go.mod h1:e04SN0xO1UNJ1M5GP0vzVBFicIe4O53FOfcixIqTyXo= cloud.google.com/go/oslogin v1.9.0/go.mod h1:HNavntnH8nzrn8JCTT5fj18FuJLFJc4NaZJtBnQtKFs= cloud.google.com/go/phishingprotection v0.5.0/go.mod h1:Y3HZknsK9bc9dMi+oE8Bim0lczMU6hrX0UpADuMefr0= cloud.google.com/go/phishingprotection v0.6.0/go.mod h1:9Y3LBLgy0kDTcYET8ZH3bq/7qni15yVUoAxiFxnlSUA= cloud.google.com/go/phishingprotection v0.7.0/go.mod h1:8qJI4QKHoda/sb/7/YmMQ2omRLSLYSu9bU0EKCNI+Lk= cloud.google.com/go/policytroubleshooter v1.3.0/go.mod h1:qy0+VwANja+kKrjlQuOzmlvscn4RNsAc0e15GGqfMxg= cloud.google.com/go/policytroubleshooter v1.4.0/go.mod h1:DZT4BcRw3QoO8ota9xw/LKtPa8lKeCByYeKTIf/vxdE= cloud.google.com/go/policytroubleshooter v1.5.0/go.mod h1:Rz1WfV+1oIpPdN2VvvuboLVRsB1Hclg3CKQ53j9l8vw= cloud.google.com/go/policytroubleshooter v1.6.0/go.mod h1:zYqaPTsmfvpjm5ULxAyD/lINQxJ0DDsnWOP/GZ7xzBc= cloud.google.com/go/privatecatalog v0.5.0/go.mod h1:XgosMUvvPyxDjAVNDYxJ7wBW8//hLDDYmnsNcMGq1K0= cloud.google.com/go/privatecatalog v0.6.0/go.mod h1:i/fbkZR0hLN29eEWiiwue8Pb+GforiEIBnV9yrRUOKI= cloud.google.com/go/privatecatalog v0.7.0/go.mod h1:2s5ssIFO69F5csTXcwBP7NPFTZvps26xGzvQ2PQaBYg= cloud.google.com/go/privatecatalog v0.8.0/go.mod h1:nQ6pfaegeDAq/Q5lrfCQzQLhubPiZhSaNhIgfJlnIXs= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= cloud.google.com/go/pubsub v1.26.0/go.mod h1:QgBH3U/jdJy/ftjPhTkyXNj543Tin1pRYcdcPRnFIRI= cloud.google.com/go/pubsub v1.27.1/go.mod h1:hQN39ymbV9geqBnfQq6Xf63yNhUAhv9CZhzp5O6qsW0= cloud.google.com/go/pubsub v1.28.0/go.mod h1:vuXFpwaVoIPQMGXqRyUQigu/AX1S3IWugR9xznmcXX8= cloud.google.com/go/pubsub v1.30.0/go.mod h1:qWi1OPS0B+b5L+Sg6Gmc9zD1Y+HaM0MdUr7LsupY1P4= cloud.google.com/go/pubsublite v1.5.0/go.mod h1:xapqNQ1CuLfGi23Yda/9l4bBCKz/wC3KIJ5gKcxveZg= cloud.google.com/go/pubsublite v1.6.0/go.mod h1:1eFCS0U11xlOuMFV/0iBqw3zP12kddMeCbj/F3FSj9k= cloud.google.com/go/pubsublite v1.7.0/go.mod h1:8hVMwRXfDfvGm3fahVbtDbiLePT3gpoiJYJY+vxWxVM= cloud.google.com/go/recaptchaenterprise v1.3.1/go.mod h1:OdD+q+y4XGeAlxRaMn1Y7/GveP6zmq76byL6tjPE7d4= cloud.google.com/go/recaptchaenterprise/v2 v2.1.0/go.mod h1:w9yVqajwroDNTfGuhmOjPDN//rZGySaf6PtFVcSCa7o= cloud.google.com/go/recaptchaenterprise/v2 v2.2.0/go.mod h1:/Zu5jisWGeERrd5HnlS3EUGb/D335f9k51B/FVil0jk= cloud.google.com/go/recaptchaenterprise/v2 v2.3.0/go.mod h1:O9LwGCjrhGHBQET5CA7dd5NwwNQUErSgEDit1DLNTdo= cloud.google.com/go/recaptchaenterprise/v2 v2.4.0/go.mod h1:Am3LHfOuBstrLrNCBrlI5sbwx9LBg3te2N6hGvHn2mE= cloud.google.com/go/recaptchaenterprise/v2 v2.5.0/go.mod h1:O8LzcHXN3rz0j+LBC91jrwI3R+1ZSZEWrfL7XHgNo9U= cloud.google.com/go/recaptchaenterprise/v2 v2.6.0/go.mod h1:RPauz9jeLtB3JVzg6nCbe12qNoaa8pXc4d/YukAmcnA= cloud.google.com/go/recaptchaenterprise/v2 v2.7.0/go.mod h1:19wVj/fs5RtYtynAPJdDTb69oW0vNHYDBTbB4NvMD9c= cloud.google.com/go/recommendationengine v0.5.0/go.mod h1:E5756pJcVFeVgaQv3WNpImkFP8a+RptV6dDLGPILjvg= cloud.google.com/go/recommendationengine v0.6.0/go.mod h1:08mq2umu9oIqc7tDy8sx+MNJdLG0fUi3vaSVbztHgJ4= cloud.google.com/go/recommendationengine v0.7.0/go.mod h1:1reUcE3GIu6MeBz/h5xZJqNLuuVjNg1lmWMPyjatzac= cloud.google.com/go/recommender v1.5.0/go.mod h1:jdoeiBIVrJe9gQjwd759ecLJbxCDED4A6p+mqoqDvTg= cloud.google.com/go/recommender v1.6.0/go.mod h1:+yETpm25mcoiECKh9DEScGzIRyDKpZ0cEhWGo+8bo+c= cloud.google.com/go/recommender v1.7.0/go.mod h1:XLHs/W+T8olwlGOgfQenXBTbIseGclClff6lhFVe9Bs= cloud.google.com/go/recommender v1.8.0/go.mod h1:PkjXrTT05BFKwxaUxQmtIlrtj0kph108r02ZZQ5FE70= cloud.google.com/go/recommender v1.9.0/go.mod h1:PnSsnZY7q+VL1uax2JWkt/UegHssxjUVVCrX52CuEmQ= cloud.google.com/go/redis v1.7.0/go.mod h1:V3x5Jq1jzUcg+UNsRvdmsfuFnit1cfe3Z/PGyq/lm4Y= cloud.google.com/go/redis v1.8.0/go.mod h1:Fm2szCDavWzBk2cDKxrkmWBqoCiL1+Ctwq7EyqBCA/A= cloud.google.com/go/redis v1.9.0/go.mod h1:HMYQuajvb2D0LvMgZmLDZW8V5aOC/WxstZHiy4g8OiA= cloud.google.com/go/redis v1.10.0/go.mod h1:ThJf3mMBQtW18JzGgh41/Wld6vnDDc/F/F35UolRZPM= cloud.google.com/go/redis v1.11.0/go.mod h1:/X6eicana+BWcUda5PpwZC48o37SiFVTFSs0fWAJ7uQ= cloud.google.com/go/resourcemanager v1.3.0/go.mod h1:bAtrTjZQFJkiWTPDb1WBjzvc6/kifjj4QBYuKCCoqKA= cloud.google.com/go/resourcemanager v1.4.0/go.mod h1:MwxuzkumyTX7/a3n37gmsT3py7LIXwrShilPh3P1tR0= cloud.google.com/go/resourcemanager v1.5.0/go.mod h1:eQoXNAiAvCf5PXxWxXjhKQoTMaUSNrEfg+6qdf/wots= cloud.google.com/go/resourcemanager v1.6.0/go.mod h1:YcpXGRs8fDzcUl1Xw8uOVmI8JEadvhRIkoXXUNVYcVo= cloud.google.com/go/resourcemanager v1.7.0/go.mod h1:HlD3m6+bwhzj9XCouqmeiGuni95NTrExfhoSrkC/3EI= cloud.google.com/go/resourcesettings v1.3.0/go.mod h1:lzew8VfESA5DQ8gdlHwMrqZs1S9V87v3oCnKCWoOuQU= cloud.google.com/go/resourcesettings v1.4.0/go.mod h1:ldiH9IJpcrlC3VSuCGvjR5of/ezRrOxFtpJoJo5SmXg= cloud.google.com/go/resourcesettings v1.5.0/go.mod h1:+xJF7QSG6undsQDfsCJyqWXyBwUoJLhetkRMDRnIoXA= cloud.google.com/go/retail v1.8.0/go.mod h1:QblKS8waDmNUhghY2TI9O3JLlFk8jybHeV4BF19FrE4= cloud.google.com/go/retail v1.9.0/go.mod h1:g6jb6mKuCS1QKnH/dpu7isX253absFl6iE92nHwlBUY= cloud.google.com/go/retail v1.10.0/go.mod h1:2gDk9HsL4HMS4oZwz6daui2/jmKvqShXKQuB2RZ+cCc= cloud.google.com/go/retail v1.11.0/go.mod h1:MBLk1NaWPmh6iVFSz9MeKG/Psyd7TAgm6y/9L2B4x9Y= cloud.google.com/go/retail v1.12.0/go.mod h1:UMkelN/0Z8XvKymXFbD4EhFJlYKRx1FGhQkVPU5kF14= cloud.google.com/go/run v0.2.0/go.mod h1:CNtKsTA1sDcnqqIFR3Pb5Tq0usWxJJvsWOCPldRU3Do= cloud.google.com/go/run v0.3.0/go.mod h1:TuyY1+taHxTjrD0ZFk2iAR+xyOXEA0ztb7U3UNA0zBo= cloud.google.com/go/run v0.8.0/go.mod h1:VniEnuBwqjigv0A7ONfQUaEItaiCRVujlMqerPPiktM= cloud.google.com/go/run v0.9.0/go.mod h1:Wwu+/vvg8Y+JUApMwEDfVfhetv30hCG4ZwDR/IXl2Qg= cloud.google.com/go/scheduler v1.4.0/go.mod h1:drcJBmxF3aqZJRhmkHQ9b3uSSpQoltBPGPxGAWROx6s= cloud.google.com/go/scheduler v1.5.0/go.mod h1:ri073ym49NW3AfT6DZi21vLZrG07GXr5p3H1KxN5QlI= cloud.google.com/go/scheduler v1.6.0/go.mod h1:SgeKVM7MIwPn3BqtcBntpLyrIJftQISRrYB5ZtT+KOk= cloud.google.com/go/scheduler v1.7.0/go.mod h1:jyCiBqWW956uBjjPMMuX09n3x37mtyPJegEWKxRsn44= cloud.google.com/go/scheduler v1.8.0/go.mod h1:TCET+Y5Gp1YgHT8py4nlg2Sew8nUHMqcpousDgXJVQc= cloud.google.com/go/scheduler v1.9.0/go.mod h1:yexg5t+KSmqu+njTIh3b7oYPheFtBWGcbVUYF1GGMIc= cloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISIDIrId8qE5QGgKA= cloud.google.com/go/secretmanager v1.8.0/go.mod h1:hnVgi/bN5MYHd3Gt0SPuTPPp5ENina1/LxM+2W9U9J4= cloud.google.com/go/secretmanager v1.9.0/go.mod h1:b71qH2l1yHmWQHt9LC80akm86mX8AL6X1MA01dW8ht4= cloud.google.com/go/secretmanager v1.10.0/go.mod h1:MfnrdvKMPNra9aZtQFvBcvRU54hbPD8/HayQdlUgJpU= cloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4= cloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0= cloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU= cloud.google.com/go/security v1.9.0/go.mod h1:6Ta1bO8LXI89nZnmnsZGp9lVoVWXqsVbIq/t9dzI+2Q= cloud.google.com/go/security v1.10.0/go.mod h1:QtOMZByJVlibUT2h9afNDWRZ1G96gVywH8T5GUSb9IA= cloud.google.com/go/security v1.12.0/go.mod h1:rV6EhrpbNHrrxqlvW0BWAIawFWq3X90SduMJdFwtLB8= cloud.google.com/go/security v1.13.0/go.mod h1:Q1Nvxl1PAgmeW0y3HTt54JYIvUdtcpYKVfIB8AOMZ+0= cloud.google.com/go/securitycenter v1.13.0/go.mod h1:cv5qNAqjY84FCN6Y9z28WlkKXyWsgLO832YiWwkCWcU= cloud.google.com/go/securitycenter v1.14.0/go.mod h1:gZLAhtyKv85n52XYWt6RmeBdydyxfPeTrpToDPw4Auc= cloud.google.com/go/securitycenter v1.15.0/go.mod h1:PeKJ0t8MoFmmXLXWm41JidyzI3PJjd8sXWaVqg43WWk= cloud.google.com/go/securitycenter v1.16.0/go.mod h1:Q9GMaLQFUD+5ZTabrbujNWLtSLZIZF7SAR0wWECrjdk= cloud.google.com/go/securitycenter v1.18.1/go.mod h1:0/25gAzCM/9OL9vVx4ChPeM/+DlfGQJDwBy/UC8AKK0= cloud.google.com/go/securitycenter v1.19.0/go.mod h1:LVLmSg8ZkkyaNy4u7HCIshAngSQ8EcIRREP3xBnyfag= cloud.google.com/go/servicecontrol v1.4.0/go.mod h1:o0hUSJ1TXJAmi/7fLJAedOovnujSEvjKCAFNXPQ1RaU= cloud.google.com/go/servicecontrol v1.5.0/go.mod h1:qM0CnXHhyqKVuiZnGKrIurvVImCs8gmqWsDoqe9sU1s= cloud.google.com/go/servicecontrol v1.10.0/go.mod h1:pQvyvSRh7YzUF2efw7H87V92mxU8FnFDawMClGCNuAA= cloud.google.com/go/servicecontrol v1.11.0/go.mod h1:kFmTzYzTUIuZs0ycVqRHNaNhgR+UMUpw9n02l/pY+mc= cloud.google.com/go/servicecontrol v1.11.1/go.mod h1:aSnNNlwEFBY+PWGQ2DoM0JJ/QUXqV5/ZD9DOLB7SnUk= cloud.google.com/go/servicedirectory v1.4.0/go.mod h1:gH1MUaZCgtP7qQiI+F+A+OpeKF/HQWgtAddhTbhL2bs= cloud.google.com/go/servicedirectory v1.5.0/go.mod h1:QMKFL0NUySbpZJ1UZs3oFAmdvVxhhxB6eJ/Vlp73dfg= cloud.google.com/go/servicedirectory v1.6.0/go.mod h1:pUlbnWsLH9c13yGkxCmfumWEPjsRs1RlmJ4pqiNjVL4= cloud.google.com/go/servicedirectory v1.7.0/go.mod h1:5p/U5oyvgYGYejufvxhgwjL8UVXjkuw7q5XcG10wx1U= cloud.google.com/go/servicedirectory v1.8.0/go.mod h1:srXodfhY1GFIPvltunswqXpVxFPpZjf8nkKQT7XcXaY= cloud.google.com/go/servicedirectory v1.9.0/go.mod h1:29je5JjiygNYlmsGz8k6o+OZ8vd4f//bQLtvzkPPT/s= cloud.google.com/go/servicemanagement v1.4.0/go.mod h1:d8t8MDbezI7Z2R1O/wu8oTggo3BI2GKYbdG4y/SJTco= cloud.google.com/go/servicemanagement v1.5.0/go.mod h1:XGaCRe57kfqu4+lRxaFEAuqmjzF0r+gWHjWqKqBvKFo= cloud.google.com/go/servicemanagement v1.6.0/go.mod h1:aWns7EeeCOtGEX4OvZUWCCJONRZeFKiptqKf1D0l/Jc= cloud.google.com/go/servicemanagement v1.8.0/go.mod h1:MSS2TDlIEQD/fzsSGfCdJItQveu9NXnUniTrq/L8LK4= cloud.google.com/go/serviceusage v1.3.0/go.mod h1:Hya1cozXM4SeSKTAgGXgj97GlqUvF5JaoXacR1JTP/E= cloud.google.com/go/serviceusage v1.4.0/go.mod h1:SB4yxXSaYVuUBYUml6qklyONXNLt83U0Rb+CXyhjEeU= cloud.google.com/go/serviceusage v1.5.0/go.mod h1:w8U1JvqUqwJNPEOTQjrMHkw3IaIFLoLsPLvsE3xueec= cloud.google.com/go/serviceusage v1.6.0/go.mod h1:R5wwQcbOWsyuOfbP9tGdAnCAc6B9DRwPG1xtWMDeuPA= cloud.google.com/go/shell v1.3.0/go.mod h1:VZ9HmRjZBsjLGXusm7K5Q5lzzByZmJHf1d0IWHEN5X4= cloud.google.com/go/shell v1.4.0/go.mod h1:HDxPzZf3GkDdhExzD/gs8Grqk+dmYcEjGShZgYa9URw= cloud.google.com/go/shell v1.6.0/go.mod h1:oHO8QACS90luWgxP3N9iZVuEiSF84zNyLytb+qE2f9A= cloud.google.com/go/spanner v1.41.0/go.mod h1:MLYDBJR/dY4Wt7ZaMIQ7rXOTLjYrmxLE/5ve9vFfWos= cloud.google.com/go/spanner v1.44.0/go.mod h1:G8XIgYdOK+Fbcpbs7p2fiprDw4CaZX63whnSMLVBxjk= cloud.google.com/go/spanner v1.45.0/go.mod h1:FIws5LowYz8YAE1J8fOS7DJup8ff7xJeetWEo5REA2M= cloud.google.com/go/speech v1.6.0/go.mod h1:79tcr4FHCimOp56lwC01xnt/WPJZc4v3gzyT7FoBkCM= cloud.google.com/go/speech v1.7.0/go.mod h1:KptqL+BAQIhMsj1kOP2la5DSEEerPDuOP/2mmkhHhZQ= cloud.google.com/go/speech v1.8.0/go.mod h1:9bYIl1/tjsAnMgKGHKmBZzXKEkGgtU+MpdDPTE9f7y0= cloud.google.com/go/speech v1.9.0/go.mod h1:xQ0jTcmnRFFM2RfX/U+rk6FQNUF6DQlydUSyoooSpco= cloud.google.com/go/speech v1.14.1/go.mod h1:gEosVRPJ9waG7zqqnsHpYTOoAS4KouMRLDFMekpJ0J0= cloud.google.com/go/speech v1.15.0/go.mod h1:y6oH7GhqCaZANH7+Oe0BhgIogsNInLlz542tg3VqeYI= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc= cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s= cloud.google.com/go/storage v1.28.1/go.mod h1:Qnisd4CqDdo6BGs2AD5LLnEsmSQ80wQ5ogcBBKhU86Y= cloud.google.com/go/storage v1.29.0/go.mod h1:4puEjyTKnku6gfKoTfNOU/W+a9JyuVNxjpS5GBrB8h4= cloud.google.com/go/storage v1.50.0 h1:3TbVkzTooBvnZsk7WaAQfOsNrdoM8QHusXA1cpk6QJs= cloud.google.com/go/storage v1.50.0/go.mod h1:l7XeiD//vx5lfqE3RavfmU9yvk5Pp0Zhcv482poyafY= cloud.google.com/go/storagetransfer v1.5.0/go.mod h1:dxNzUopWy7RQevYFHewchb29POFv3/AaBgnhqzqiK0w= cloud.google.com/go/storagetransfer v1.6.0/go.mod h1:y77xm4CQV/ZhFZH75PLEXY0ROiS7Gh6pSKrM8dJyg6I= cloud.google.com/go/storagetransfer v1.7.0/go.mod h1:8Giuj1QNb1kfLAiWM1bN6dHzfdlDAVC9rv9abHot2W4= cloud.google.com/go/storagetransfer v1.8.0/go.mod h1:JpegsHHU1eXg7lMHkvf+KE5XDJ7EQu0GwNJbbVGanEw= cloud.google.com/go/talent v1.1.0/go.mod h1:Vl4pt9jiHKvOgF9KoZo6Kob9oV4lwd/ZD5Cto54zDRw= cloud.google.com/go/talent v1.2.0/go.mod h1:MoNF9bhFQbiJ6eFD3uSsg0uBALw4n4gaCaEjBw9zo8g= cloud.google.com/go/talent v1.3.0/go.mod h1:CmcxwJ/PKfRgd1pBjQgU6W3YBwiewmUzQYH5HHmSCmM= cloud.google.com/go/talent v1.4.0/go.mod h1:ezFtAgVuRf8jRsvyE6EwmbTK5LKciD4KVnHuDEFmOOA= cloud.google.com/go/talent v1.5.0/go.mod h1:G+ODMj9bsasAEJkQSzO2uHQWXHHXUomArjWQQYkqK6c= cloud.google.com/go/texttospeech v1.4.0/go.mod h1:FX8HQHA6sEpJ7rCMSfXuzBcysDAuWusNNNvN9FELDd8= cloud.google.com/go/texttospeech v1.5.0/go.mod h1:oKPLhR4n4ZdQqWKURdwxMy0uiTS1xU161C8W57Wkea4= cloud.google.com/go/texttospeech v1.6.0/go.mod h1:YmwmFT8pj1aBblQOI3TfKmwibnsfvhIBzPXcW4EBovc= cloud.google.com/go/tpu v1.3.0/go.mod h1:aJIManG0o20tfDQlRIej44FcwGGl/cD0oiRyMKG19IQ= cloud.google.com/go/tpu v1.4.0/go.mod h1:mjZaX8p0VBgllCzF6wcU2ovUXN9TONFLd7iz227X2Xg= cloud.google.com/go/tpu v1.5.0/go.mod h1:8zVo1rYDFuW2l4yZVY0R0fb/v44xLh3llq7RuV61fPM= cloud.google.com/go/trace v1.3.0/go.mod h1:FFUE83d9Ca57C+K8rDl/Ih8LwOzWIV1krKgxg6N0G28= cloud.google.com/go/trace v1.4.0/go.mod h1:UG0v8UBqzusp+z63o7FK74SdFE+AXpCLdFb1rshXG+Y= cloud.google.com/go/trace v1.8.0/go.mod h1:zH7vcsbAhklH8hWFig58HvxcxyQbaIqMarMg9hn5ECA= cloud.google.com/go/trace v1.9.0/go.mod h1:lOQqpE5IaWY0Ixg7/r2SjixMuc6lfTFeO4QGM4dQWOk= cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4= cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI= cloud.google.com/go/translate v1.3.0/go.mod h1:gzMUwRjvOqj5i69y/LYLd8RrNQk+hOmIXTi9+nb3Djs= cloud.google.com/go/translate v1.4.0/go.mod h1:06Dn/ppvLD6WvA5Rhdp029IX2Mi3Mn7fpMRLPvXT5Wg= cloud.google.com/go/translate v1.5.0/go.mod h1:29YDSYveqqpA1CQFD7NQuP49xymq17RXNaUDdc0mNu0= cloud.google.com/go/translate v1.6.0/go.mod h1:lMGRudH1pu7I3n3PETiOB2507gf3HnfLV8qlkHZEyos= cloud.google.com/go/translate v1.7.0/go.mod h1:lMGRudH1pu7I3n3PETiOB2507gf3HnfLV8qlkHZEyos= cloud.google.com/go/vertexai v0.13.2 h1:dOnvkMDZy3GdKAz8Isd2d6KV3jQpk6CKvYao1SIupuk= cloud.google.com/go/vertexai v0.13.2/go.mod h1:+nmz1z8AeYILA5QM2yii3CED1PqGknZH1CUNDVatIg4= cloud.google.com/go/video v1.8.0/go.mod h1:sTzKFc0bUSByE8Yoh8X0mn8bMymItVGPfTuUBUyRgxk= cloud.google.com/go/video v1.9.0/go.mod h1:0RhNKFRF5v92f8dQt0yhaHrEuH95m068JYOvLZYnJSw= cloud.google.com/go/video v1.12.0/go.mod h1:MLQew95eTuaNDEGriQdcYn0dTwf9oWiA4uYebxM5kdg= cloud.google.com/go/video v1.13.0/go.mod h1:ulzkYlYgCp15N2AokzKjy7MQ9ejuynOJdf1tR5lGthk= cloud.google.com/go/video v1.14.0/go.mod h1:SkgaXwT+lIIAKqWAJfktHT/RbgjSuY6DobxEp0C5yTQ= cloud.google.com/go/video v1.15.0/go.mod h1:SkgaXwT+lIIAKqWAJfktHT/RbgjSuY6DobxEp0C5yTQ= cloud.google.com/go/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU= cloud.google.com/go/videointelligence v1.7.0/go.mod h1:k8pI/1wAhjznARtVT9U1llUaFNPh7muw8QyOUpavru4= cloud.google.com/go/videointelligence v1.8.0/go.mod h1:dIcCn4gVDdS7yte/w+koiXn5dWVplOZkE+xwG9FgK+M= cloud.google.com/go/videointelligence v1.9.0/go.mod h1:29lVRMPDYHikk3v8EdPSaL8Ku+eMzDljjuvRs105XoU= cloud.google.com/go/videointelligence v1.10.0/go.mod h1:LHZngX1liVtUhZvi2uNS0VQuOzNi2TkY1OakiuoUOjU= cloud.google.com/go/vision v1.2.0/go.mod h1:SmNwgObm5DpFBme2xpyOyasvBc1aPdjvMk2bBk0tKD0= cloud.google.com/go/vision/v2 v2.2.0/go.mod h1:uCdV4PpN1S0jyCyq8sIM42v2Y6zOLkZs+4R9LrGYwFo= cloud.google.com/go/vision/v2 v2.3.0/go.mod h1:UO61abBx9QRMFkNBbf1D8B1LXdS2cGiiCRx0vSpZoUo= cloud.google.com/go/vision/v2 v2.4.0/go.mod h1:VtI579ll9RpVTrdKdkMzckdnwMyX2JILb+MhPqRbPsY= cloud.google.com/go/vision/v2 v2.5.0/go.mod h1:MmaezXOOE+IWa+cS7OhRRLK2cNv1ZL98zhqFFZaaH2E= cloud.google.com/go/vision/v2 v2.6.0/go.mod h1:158Hes0MvOS9Z/bDMSFpjwsUrZ5fPrdwuyyvKSGAGMY= cloud.google.com/go/vision/v2 v2.7.0/go.mod h1:H89VysHy21avemp6xcf9b9JvZHVehWbET0uT/bcuY/0= cloud.google.com/go/vmmigration v1.2.0/go.mod h1:IRf0o7myyWFSmVR1ItrBSFLFD/rJkfDCUTO4vLlJvsE= cloud.google.com/go/vmmigration v1.3.0/go.mod h1:oGJ6ZgGPQOFdjHuocGcLqX4lc98YQ7Ygq8YQwHh9A7g= cloud.google.com/go/vmmigration v1.5.0/go.mod h1:E4YQ8q7/4W9gobHjQg4JJSgXXSgY21nA5r8swQV+Xxc= cloud.google.com/go/vmmigration v1.6.0/go.mod h1:bopQ/g4z+8qXzichC7GW1w2MjbErL54rk3/C843CjfY= cloud.google.com/go/vmwareengine v0.1.0/go.mod h1:RsdNEf/8UDvKllXhMz5J40XxDrNJNN4sagiox+OI208= cloud.google.com/go/vmwareengine v0.2.2/go.mod h1:sKdctNJxb3KLZkE/6Oui94iw/xs9PRNC2wnNLXsHvH8= cloud.google.com/go/vmwareengine v0.3.0/go.mod h1:wvoyMvNWdIzxMYSpH/R7y2h5h3WFkx6d+1TIsP39WGY= cloud.google.com/go/vpcaccess v1.4.0/go.mod h1:aQHVbTWDYUR1EbTApSVvMq1EnT57ppDmQzZ3imqIk4w= cloud.google.com/go/vpcaccess v1.5.0/go.mod h1:drmg4HLk9NkZpGfCmZ3Tz0Bwnm2+DKqViEpeEpOq0m8= cloud.google.com/go/vpcaccess v1.6.0/go.mod h1:wX2ILaNhe7TlVa4vC5xce1bCnqE3AeH27RV31lnmZes= cloud.google.com/go/webrisk v1.4.0/go.mod h1:Hn8X6Zr+ziE2aNd8SliSDWpEnSS1u4R9+xXZmFiHmGE= cloud.google.com/go/webrisk v1.5.0/go.mod h1:iPG6fr52Tv7sGk0H6qUFzmL3HHZev1htXuWDEEsqMTg= cloud.google.com/go/webrisk v1.6.0/go.mod h1:65sW9V9rOosnc9ZY7A7jsy1zoHS5W9IAXv6dGqhMQMc= cloud.google.com/go/webrisk v1.7.0/go.mod h1:mVMHgEYH0r337nmt1JyLthzMr6YxwN1aAIEc2fTcq7A= cloud.google.com/go/webrisk v1.8.0/go.mod h1:oJPDuamzHXgUc+b8SiHRcVInZQuybnvEW72PqTc7sSg= cloud.google.com/go/websecurityscanner v1.3.0/go.mod h1:uImdKm2wyeXQevQJXeh8Uun/Ym1VqworNDlBXQevGMo= cloud.google.com/go/websecurityscanner v1.4.0/go.mod h1:ebit/Fp0a+FWu5j4JOmJEV8S8CzdTkAS77oDsiSqYWQ= cloud.google.com/go/websecurityscanner v1.5.0/go.mod h1:Y6xdCPy81yi0SQnDY1xdNTNpfY1oAgXUlcfN3B3eSng= cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0= cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M= cloud.google.com/go/workflows v1.8.0/go.mod h1:ysGhmEajwZxGn1OhGOGKsTXc5PyxOc0vfKf5Af+to4M= cloud.google.com/go/workflows v1.9.0/go.mod h1:ZGkj1aFIOd9c8Gerkjjq7OW7I5+l6cSvT3ujaO/WwSA= cloud.google.com/go/workflows v1.10.0/go.mod h1:fZ8LmRmZQWacon9UCX1r/g/DfAXx5VcPALq2CxzdePw= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc= github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4= github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8= github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4= github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5 v5.7.0 h1:LkHbJbgF3YyvC53aqYGR+wWQDn2Rdp9AQdGndf9QvY4= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5 v5.7.0/go.mod h1:QyiQdW4f4/BIfB8ZutZ2s+28RAgfa/pT+zS++ZHyM1I= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v4 v4.3.0 h1:bXwSugBiSbgtz7rOtbfGf+woewp4f06orW9OP5BjHLA= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v4 v4.3.0/go.mod h1:Y/HgrePTmGy9HjdSGTqZNa+apUpTVIEVKXJyARP2lrk= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0 h1:PiSrjRPpkQNjrM8H0WwKMnZUdu1RGMtd/LdGKUrOo+c= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0/go.mod h1:oDrbWx4ewMylP7xHivfgixbfGBT6APAwsSoHRKotnIc= github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.5.0 h1:mlmW46Q0B79I+Aj4azKC6xDMFN9a9SyZWESlGWYXbFs= github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.5.0/go.mod h1:PXe2h+LKcWTX9afWdZoHyODqR4fBa5boUM/8uJfZ0Jo= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs= github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs= github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/Code-Hex/go-generics-cache v1.5.1 h1:6vhZGc5M7Y/YD8cIUcY8kcuQLB4cHR7U+0KMqAA0KcU= github.com/Code-Hex/go-generics-cache v1.5.1/go.mod h1:qxcC9kRVrct9rHeiYpFWSoW1vxyillCVzX13KZG8dl4= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 h1:ErKg/3iS1AKcTkf3yixlZ54f9U1rljCkQyEXWUnIUxc= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.50.0 h1:5IT7xOdq17MtcdtL/vtl6mGfzhaq4m4vpollPRmlsBQ= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.50.0/go.mod h1:ZV4VOm0/eHR06JLrXWe09068dHpr3TRpY9Uo7T+anuA= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.50.0 h1:nNMpRpnkWDAaqcpxMJvxa/Ud98gjbYwayJY4/9bdjiU= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.50.0/go.mod h1:SZiPHWGOOk3bl8tkevxkoiwPgsIl6CwrWcbwjfHZpdM= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.50.0 h1:ig/FpDD2JofP/NExKQUbn7uOSZzJAQqogfqluZK4ed4= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.50.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0= github.com/IBM/watsonx-go v1.0.1 h1:Juj90I8ZpJWR/Oq4ISJzQhMJwI6q+DAaZy+f/8W7KDA= github.com/IBM/watsonx-go v1.0.1/go.mod h1:8lzvpe/158JkrzvcoIcIj6OdNty5iC9co5nQHfkhRtM= github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs= github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8= github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII= github.com/MarvinJWendt/testza v0.2.10/go.mod h1:pd+VWsoGUiFtq+hRKSU1Bktnn+DMCSrDrXDpX2bG66k= github.com/MarvinJWendt/testza v0.2.12/go.mod h1:JOIegYyV7rX+7VZ9r77L/eH6CfJHHzXjB69adAhzZkI= github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/2oUqKc6bF2c= github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE= github.com/MarvinJWendt/testza v0.5.2 h1:53KDo64C1z/h/d/stCYCPY69bt/OSwjq5KpFNwi+zB4= github.com/MarvinJWendt/testza v0.5.2/go.mod h1:xu53QFE5sCdjtMCKk8YMQ2MnymimEctc4n3EjyIYvEY= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/hcsshim v0.12.4 h1:Ev7YUMHAHoWNm+aDSPzc5W9s6E2jyL1szpVDJeZ/Rr4= github.com/Microsoft/hcsshim v0.12.4/go.mod h1:Iyl1WVpZzr+UkzjekHZbV8o5Z9ZkxNGx6CtY2Qg/JVQ= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs= github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= github.com/agiledragon/gomonkey/v2 v2.13.0 h1:B24Jg6wBI1iB8EFR1c+/aoTg7QN/Cum7YffG8KMIyYo= github.com/agiledragon/gomonkey/v2 v2.13.0/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY= github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY= github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0= github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0= github.com/apache/arrow/go/v11 v11.0.0/go.mod h1:Eg5OsL5H+e299f7u5ssuXsuHQVEGC4xei5aX110hRiI= github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU= github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA= github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE= github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM= github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 h1:zAybnyUQXIZ5mok5Jqwlf58/TFE7uvd3IAsa1aF9cXs= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10/go.mod h1:qqvMj6gHLR/EXWZw4ZbqlPbQUyenf4h82UQUlKc+l14= github.com/aws/aws-sdk-go-v2/config v1.29.14 h1:f+eEi/2cKCg9pqKBoAIwRGzVb70MRKqWX4dg1BDcSJM= github.com/aws/aws-sdk-go-v2/config v1.29.14/go.mod h1:wVPHWcIFv3WO89w0rE10gzf17ZYy+UVS1Geq8Iei34g= github.com/aws/aws-sdk-go-v2/credentials v1.17.67 h1:9KxtdcIA/5xPNQyZRgUSpYOE6j9Bc4+D7nZua0KGYOM= github.com/aws/aws-sdk-go-v2/credentials v1.17.67/go.mod h1:p3C44m+cfnbv763s52gCqrjaqyPikj9Sg47kUVaNZQQ= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M= github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q= github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= github.com/aws/aws-sdk-go-v2/service/bedrock v1.33.0 h1:2P70khV5KDzoRs8UuplU3rAzzyLaj5kzND33Jutwpbg= github.com/aws/aws-sdk-go-v2/service/bedrock v1.33.0/go.mod h1:rZOgAxQVRg9v5ZEQHrrKw0Gkb9DBAASeeRiwUmmXcG0= github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.30.0 h1:eMOwQ8ZZK+76+08RfxeaGUtRFN6wxmD1rvqovc2kq2w= github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.30.0/go.mod h1:0b5Rq7rUvSQFYHI1UO0zFTV/S6j6DUyuykXA80C+YOI= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY= github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 h1:1Gw+9ajCV1jogloEv1RRnvfRFia2cL6c9cuKV2Ps+G8= github.com/aws/aws-sdk-go-v2/service/sso v1.25.3/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 h1:hXmVKytPfTy5axZ+fYbR5d0cFmC3JvwLm5kM83luako= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs= github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 h1:1XuUZ8mYJw9B6lzAkXhqHlJd/XvaX32evhproijJEZY= github.com/aws/aws-sdk-go-v2/service/sts v1.33.19/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4= github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ= github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70= github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd h1:rFt+Y/IK1aEZkEHchZRSq9OQbsSzIT/OrI8YFFmRIng= github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b h1:otBG+dV+YK+Soembjv71DPz3uX/V/6MMlSyD9JBQ6kQ= github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXer/kZD8Ri1aaunCxIEsOst1BVJswV0o= github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chai2010/gettext-go v1.0.3 h1:9liNh8t+u26xl5ddmWLmsOsdNLwkdRTg5AG+JnTiM80= github.com/chai2010/gettext-go v1.0.3/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM= github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f h1:C5bqEmzEPLsHm9Mv73lSE9e9bKV23aB1vxOsmZrkl3k= github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/cohere-ai/cohere-go/v2 v2.12.2 h1:8WJqqcCe3q6TB1CdhgzJOgRO2ouno8xcYcOoeWtI8Pk= github.com/cohere-ai/cohere-go/v2 v2.12.2/go.mod h1:MuiJkCxlR18BDV2qQPbz2Yb/OCVphT1y6nD2zYaKeR0= github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= github.com/containerd/cgroups/v3 v3.0.2 h1:f5WFqIVSgo5IZmtTT3qVBo6TzI1ON6sycSBKkymb9L0= github.com/containerd/cgroups/v3 v3.0.2/go.mod h1:JUgITrzdFqp42uI2ryGA+ge0ap/nxzYgkGmIcetmErE= github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro= github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/containerd/containerd v1.7.29 h1:90fWABQsaN9mJhGkoVnuzEY+o1XDPbg9BTC9QTAHnuE= github.com/containerd/containerd v1.7.29/go.mod h1:azUkWcOvHrWvaiUjSQH0fjzuHIwSPg1WL5PshGP4Szs= github.com/containerd/continuity v0.4.4 h1:/fNVfTJ7wIl/YPMHjf+5H32uFhl63JucB34PlCpMKII= github.com/containerd/continuity v0.4.4/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM= github.com/cyphar/filepath-securejoin v0.3.6/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/digitalocean/godo v1.157.0 h1:ReELaS6FxXNf8gryUiVH0wmyUmZN8/NCmBX4gXd3F0o= github.com/digitalocean/godo v1.157.0/go.mod h1:tYeiWY5ZXVpU48YaFv0M5irUFHXGorZpDNm7zzdWMzM= github.com/distribution/distribution/v3 v3.0.0-20221208165359-362910506bc2 h1:aBfCb7iqHmDEIp6fBvC/hQUddQfg+3qdYjwzaiP9Hnc= github.com/distribution/distribution/v3 v3.0.0-20221208165359-362910506bc2/go.mod h1:WHNsWjnIn2V1LYOrME7e8KxSeKunYHsxEm4am0BUtcI= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/docker/cli v26.1.4+incompatible h1:I8PHdc0MtxEADqYJZvhBrW9bo8gawKwwenxRM7/rLu8= github.com/docker/cli v26.1.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v28.0.4+incompatible h1:JNNkBctYKurkw6FrHfKqY0nKIDf5nrbxjVBtS+cdcok= github.com/docker/docker v28.0.4+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4= github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU= github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/go-control-plane v0.10.3/go.mod h1:fJJn/j26vwOu972OllsvAgJJM//w9BV6Fxbg2LuVd34= github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo= github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w= github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls= github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4= github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc= github.com/expr-lang/expr v1.17.7 h1:Q0xY/e/2aCIp8g9s/LGvMDCC5PxYlvHgDZRQ4y16JX8= github.com/expr-lang/expr v1.17.7/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/flowstack/go-jsonschema v0.1.1/go.mod h1:yL7fNggx1o8rm9RlgXv7hTBWxdBM0rVwpMwimd3F3N0= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI= github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks= github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= github.com/go-fonts/liberation v0.2.0/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U= github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM= github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-zookeeper/zk v1.0.4 h1:DPzxraQx7OrPyXq2phlGlNSIyWEsAox0RJmjTseMV6I= github.com/go-zookeeper/zk v1.0.4/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomodule/redigo v1.8.2 h1:H5XSIre1MB5NbPYFp+i1NBbb5qN1W8Y8YAQoAYbkm8k= github.com/gomodule/redigo v1.8.2/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/generative-ai-go v0.19.0 h1:R71szggh8wHMCUlEMsW2A/3T+5LdEIkiaHSYgSpUgdg= github.com/google/generative-ai-go v0.19.0/go.mod h1:JYolL13VG7j79kM5BtHz4qwONHkeJQzOCkKXnpqtS/E= github.com/google/gnostic v0.7.0 h1:d7EpuFp8vVdML+y0JJJYiKeOLjKTdH/GvVkLOBWqJpw= github.com/google/gnostic v0.7.0/go.mod h1:IAcUyMl6vtC95f60EZ8oXyqTsOersP6HbwjeG7EyDPM= github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 h1:0VpGH+cDhbDtdcweoyCVsF3fhN8kejK6rFe/2FFX2nU= github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49/go.mod h1:BkkQ4L1KS1xMt2aWSPStnn55ChGC0DPOn2FQYj+f25M= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20250607225305-033d6d78b36a h1://KbezygeMJZCSHH+HgUZiTeSoiuFspbMg1ge+eFj18= github.com/google/pprof v0.0.0-20250607225305-033d6d78b36a/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= github.com/googleapis/enterprise-certificate-proxy v0.2.1/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo= github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY= github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8= github.com/googleapis/gax-go/v2 v2.7.1/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI= github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3GqO0k0= github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w= github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo= github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= github.com/gophercloud/gophercloud v1.14.1 h1:DTCNaTVGl8/cFu58O1JwWgis9gtISAFONqpMKNg/Vpw= github.com/gophercloud/gophercloud/v2 v2.7.0 h1:o0m4kgVcPgHlcXiWAjoVxGd8QCmvM5VU+YM71pFbn0E= github.com/gophercloud/gophercloud/v2 v2.7.0/go.mod h1:Ki/ILhYZr/5EPebrPL9Ej+tUg4lqx71/YH2JWVeU+Qk= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY= github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248= github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= github.com/hashicorp/consul/api v1.32.0 h1:5wp5u780Gri7c4OedGEPzmlUEzi0g2KyiPphSr6zjVg= github.com/hashicorp/consul/api v1.32.0/go.mod h1:Z8YgY0eVPukT/17ejW+l+C7zJmKwgPHtjU1q16v/Y40= github.com/hashicorp/cronexpr v1.1.2 h1:wG/ZYIKT+RT3QkOdgYc+xsKWVRgnxJ1OJtjjy84fJ9A= github.com/hashicorp/cronexpr v1.1.2/go.mod h1:P4wA0KBl9C5q2hABiMO7cp6jcIg96CDh1Efb3g1PWA4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/nomad/api v0.0.0-20241218080744-e3ac00f30eec h1:+YBzb977VrmffaCX/OBm17dEVJUcWn5dW+eqs3aIJ/A= github.com/hashicorp/nomad/api v0.0.0-20241218080744-e3ac00f30eec/go.mod h1:svtxn6QnrQ69P23VvIWMR34tg3vmwLz4UdUzm1dSCgE= github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY= github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= github.com/hetznercloud/hcloud-go/v2 v2.21.1 h1:IH3liW8/cCRjfJ4cyqYvw3s1ek+KWP8dl1roa0lD8JM= github.com/hetznercloud/hcloud-go/v2 v2.21.1/go.mod h1:XOaYycZJ3XKMVWzmqQ24/+1V7ormJHmPdck/kxrNnQA= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/hupe1980/go-huggingface v0.0.15 h1:tTWmUGGunC/BYz4hrwS8SSVtMYVYjceG2uhL8HxeXvw= github.com/hupe1980/go-huggingface v0.0.15/go.mod h1:IRvsik3+b9BJyw9hCfw1arI6gDObcVto1UA8f3kt8mM= github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v1.0.1 h1:lFIgOs30GMaV/2+qQ+eEBLbUL6h1YosdohE3ODy4hTs= github.com/imdario/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= github.com/ionos-cloud/sdk-go/v6 v6.3.4 h1:jTvGl4LOF8v8OYoEIBNVwbFoqSGAFqn6vGE7sp7/BqQ= github.com/ionos-cloud/sdk-go/v6 v6.3.4/go.mod h1:wCVwNJ/21W29FWFUv+fNawOTMlFoP1dS3L+ZuztFW48= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kedacore/keda/v2 v2.16.0 h1:0ZoqAeGHORh0B/BOBLDf6fRVvgc5ATeuQCgEm6bNViM= github.com/kedacore/keda/v2 v2.16.0/go.mod h1:17Yth2jUQvi5KZGGIRmL4ZlwFZY/0FDxIG5jSa+IjpY= github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b h1:udzkj9S/zlT5X367kqJis0QP7YMxobob6zhzq6Yre00= github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b/go.mod h1:pcaDhQK0/NJZEvtCO0qQPPropqV0sJOJ6YW7X+9kRwM= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/kyverno/policy-reporter-kyverno-plugin v1.6.4 h1:mJYDLS+uGhNos+qjkDJUfeJlLs90MsDANBOhWoQfnh4= github.com/kyverno/policy-reporter-kyverno-plugin v1.6.4/go.mod h1:NMA8PofO4aPrEpw6b74COjx5/WD2EUAXWN3bAMGGxhU= github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= github.com/linode/linodego v1.52.2 h1:N9ozU27To1LMSrDd8WvJZ5STSz1eGYdyLnxhAR/dIZg= github.com/linode/linodego v1.52.2/go.mod h1:bI949fZaVchjWyKIA08hNyvAcV6BAS+PM2op3p7PAWA= github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4= github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4= github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM= github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mark3labs/mcp-go v0.36.0 h1:rIZaijrRYPeSbJG8/qNDe0hWlGrCJ7FWHNMz2SQpTis= github.com/mark3labs/mcp-go v0.36.0/go.mod h1:T7tUa2jO6MavG+3P25Oy/jR7iCeJPHImCZHRymCn39g= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE= github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE= github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mittwald/go-helm-client v0.12.14 h1:az3GJ4kRmFK609Ic3iHXveNtg92n9jWG0YpKKTIK4oo= github.com/mittwald/go-helm-client v0.12.14/go.mod h1:2VogAupgnV7FiuoPqtpCYKS/RrMh9fFA3/pD/OmTaLc= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= github.com/moby/sys/mountinfo v0.7.1 h1:/tTvQaSJRr2FshkhXiIpux6fQ2Zvc4j7tAhMTStAG2g= github.com/moby/sys/mountinfo v0.7.1/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/ollama/ollama v0.13.4 h1:COb7S3+mvXkAHG7vxqeD7uhAPJ/UCAn7OeGkiBBCo98= github.com/ollama/ollama v0.13.4/go.mod h1:2VxohsKICsmUCrBjowf+luTXYiXn2Q70Cnvv5Urbzkw= github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/oracle/oci-go-sdk/v65 v65.79.0 h1:Tv9L1XTKWkdXtSViMbP+dA93WunquvW++/2s5pOvOgU= github.com/oracle/oci-go-sdk/v65 v65.79.0/go.mod h1:IBEV9l1qBzUpo7zgGaRUhbB05BVfcDGYRFBCPlTcPp0= github.com/ovh/go-ovh v1.9.0 h1:6K8VoL3BYjVV3In9tPJUdT7qMx9h0GExN9EXx1r2kKE= github.com/ovh/go-ovh v1.9.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI= github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY= github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= github.com/prometheus/client_golang v1.23.0-rc.1 h1:Is/nGODd8OsJlNQSybeYBwY/B6aHrN7+QwVUYutHSgw= github.com/prometheus/client_golang v1.23.0-rc.1/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= github.com/prometheus/common v0.65.1-0.20250703115700-7f8b2a0d32d3 h1:R/zO7ombSHCI8bjQusgCMSL+cE669w5/R2upq5WlPD0= github.com/prometheus/common v0.65.1-0.20250703115700-7f8b2a0d32d3/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/prometheus/prometheus v0.306.0 h1:Q0Pvz/ZKS6vVWCa1VSgNyNJlEe8hxdRlKklFg7SRhNw= github.com/prometheus/prometheus v0.306.0/go.mod h1:7hMSGyZHt0dcmZ5r4kFPJ/vxPQU99N5/BGwSPDxeZrQ= github.com/prometheus/sigv4 v0.2.0 h1:qDFKnHYFswJxdzGeRP63c4HlH3Vbn1Yf/Ao2zabtVXk= github.com/prometheus/sigv4 v0.2.0/go.mod h1:D04rqmAaPPEUkjRQxGqjoxdyJuyCh6E0M18fZr0zBiE= github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI= github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg= github.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE= github.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEejaWgXU= github.com/pterm/pterm v0.12.33/go.mod h1:x+h2uL+n7CP/rel9+bImHD5lF3nM9vJj80k9ybiiTTE= github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5bUw8T8= github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s= github.com/pterm/pterm v0.12.80 h1:mM55B+GnKUnLMUSqhdINe4s6tOuVQIetQ3my8JGyAIg= github.com/pterm/pterm v0.12.80/go.mod h1:c6DeF9bSnOSeFPZlfs4ZRAFcf5SCoTwvwQ5xaKGQlHo= github.com/redis/go-redis/v9 v9.8.0 h1:q3nRvjrlge/6UD7eTu/DSg2uYiU2mCL0G/uzBWqhicI= github.com/redis/go-redis/v9 v9.8.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rubenv/sql-migrate v1.7.1 h1:f/o0WgfO/GqNuVg+6801K/KW3WdDSupzSjDYODmiUq4= github.com/rubenv/sql-migrate v1.7.1/go.mod h1:Ob2Psprc0/3ggbM6wCzyYVFFuc6FyZrb2AS+ezLDFb4= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk= github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk= github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/sashabaranov/go-openai v1.36.0 h1:fcSrn8uGuorzPWCBp8L0aCR95Zjb/Dd+ZSML0YZy9EI= github.com/sashabaranov/go-openai v1.36.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= github.com/scaleway/scaleway-sdk-go v1.0.0-beta.33 h1:KhF0WejiUTDbL5X55nXowP7zNopwpowa6qaMAWyIE+0= github.com/scaleway/scaleway-sdk-go v1.0.0-beta.33/go.mod h1:792k1RTU+5JeMXm35/e2Wgp71qPH/DmDoZrRc+EFZDk= github.com/schollz/progressbar/v3 v3.17.1 h1:bI1MTaoQO+v5kzklBjYNRQLoVpe0zbyRZNK6DFkVC5U= github.com/schollz/progressbar/v3 v3.17.1/go.mod h1:RzqpnsPQNjUyIgdglUjRLgD7sVnxN1wpmBMV+UiEbL4= github.com/segmentio/fasthash v1.0.3 h1:EI9+KE1EwvMLBWwjpRDc+fEM+prwxDYbslddQGtrmhM= github.com/segmentio/fasthash v1.0.3/go.mod h1:waKX8l2N8yckOgmSsXJi7x1ZfdKZ4x7KRMzBtS3oedY= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/sony/gobreaker v0.5.0 h1:dRCvqm0P490vZPmy7ppEk2qCnCieBooFJ+YoXGYB+yg= github.com/sony/gobreaker v0.5.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= github.com/stackitcloud/stackit-sdk-go/core v0.17.2 h1:jPyn+i8rkp2hM80+hOg0B/1EVRbMt778Tr5RWyK1m2E= github.com/stackitcloud/stackit-sdk-go/core v0.17.2/go.mod h1:8KIw3czdNJ9sdil9QQimxjR6vHjeINFrRv0iZ67wfn0= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/vultr/govultr/v2 v2.17.2 h1:gej/rwr91Puc/tgh+j33p/BLR16UrIPnSr+AIwYWZQs= github.com/vultr/govultr/v2 v2.17.2/go.mod h1:ZFOKGWmgjytfyjeyAdhQlSWwTjh2ig+X49cAp50dzXI= github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43 h1:+lm10QQTNSBd8DVTNGHx7o/IKu9HYDvLMffDhbyLccI= github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs= github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50 h1:hlE8//ciYMztlGpl/VA+Zm1AcTPHYkHJPbHqE6WJUXE= github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f h1:ERexzlUfuTvpE74urLSbIQW0Z/6hF9t8U4NsJLaioAY= github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/contrib/detectors/gcp v1.35.0 h1:bGvFt68+KTiAKFlacHW6AhA56GF2rS0bdD3aJYEnmzA= go.opentelemetry.io/contrib/detectors/gcp v1.35.0/go.mod h1:qGWP8/+ILwMRIUf9uIVLloR1uo5ZYAslM4O6OqUi1DA= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0 h1:WDdP9acbMYjbKIyJUhTvtzj601sVJOqgWdUxSdR/Ysc= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0/go.mod h1:BLbf7zbNIONBLPwvFnwNHGj4zge8uTCM/UPIVW1Mq2I= go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa h1:t2QcU6V556bFjYgu4L6C+6VrCPyJZ+eyRsABUPs1mz4= golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20210216034530-4410531fe030/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20210607152325-775e3b0c77b9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= golang.org/x/image v0.0.0-20220302094943-723b81ca9867/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= golang.org/x/oauth2 v0.0.0-20221006150949-b44042a4b9c1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec= golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0= gonum.org/v1/gonum v0.11.0/go.mod h1:fSG4YDCxxUZQJ7rKsQrj0gMOg00Il0Z96/qMA4bVQhA= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY= gonum.org/v1/plot v0.10.1/go.mod h1:VZW5OlhkL1mysU9vaqNHnsy86inf6Ot+jB3r+BczCEo= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= google.golang.org/api v0.77.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= google.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6FO2g= google.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= google.golang.org/api v0.93.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= google.golang.org/api v0.95.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI= google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= google.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= google.golang.org/api v0.99.0/go.mod h1:1YOf74vkVndF7pG6hIHuINsM7eWwpVTAfNMNiL91A08= google.golang.org/api v0.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70= google.golang.org/api v0.102.0/go.mod h1:3VFl6/fzoA+qNuS1N1/VfXY4LjoXN/wzeIp7TweWwGo= google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0= google.golang.org/api v0.106.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= google.golang.org/api v0.107.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= google.golang.org/api v0.108.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60ca7iI= google.golang.org/api v0.111.0/go.mod h1:qtFHvU9mhgTJegR31csQ+rwxyUTHOKFqCKWp1J0fdw0= google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg= google.golang.org/api v0.239.0 h1:2hZKUnFZEy81eugPs4e2XzIJ5SOwQg0G82bpXD65Puo= google.golang.org/api v0.239.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= google.golang.org/genproto v0.0.0-20220329172620-7be39ac1afc7/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= google.golang.org/genproto v0.0.0-20220722212130-b98a9ff5e252/go.mod h1:GkXuJDJ6aQ7lnJcRF+SJVgFdQhypqgl3LB1C9vabdRE= google.golang.org/genproto v0.0.0-20220801145646-83ce21fca29f/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc= google.golang.org/genproto v0.0.0-20220815135757-37a418bb8959/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= google.golang.org/genproto v0.0.0-20220817144833-d7fd3f11b9b1/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= google.golang.org/genproto v0.0.0-20220829144015-23454907ede3/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= google.golang.org/genproto v0.0.0-20220829175752-36a9c930ecbf/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= google.golang.org/genproto v0.0.0-20220913154956-18f8339a66a5/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= google.golang.org/genproto v0.0.0-20220914142337-ca0e39ece12f/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= google.golang.org/genproto v0.0.0-20220915135415-7fd63a7952de/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= google.golang.org/genproto v0.0.0-20220916172020-2692e8806bfa/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= google.golang.org/genproto v0.0.0-20220919141832-68c03719ef51/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= google.golang.org/genproto v0.0.0-20220920201722-2b89144ce006/go.mod h1:ht8XFiar2npT/g4vkk7O0WYS1sHOHbdujxbEp7CJWbw= google.golang.org/genproto v0.0.0-20220926165614-551eb538f295/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= google.golang.org/genproto v0.0.0-20220926220553-6981cbe3cfce/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e/go.mod h1:3526vdqwhZAwq4wsRUaVG555sVgsNmIjRtO7t/JH29U= google.golang.org/genproto v0.0.0-20221014173430-6e2ab493f96b/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= google.golang.org/genproto v0.0.0-20221024153911-1573dae28c9c/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= google.golang.org/genproto v0.0.0-20221024183307-1bc688fe9f3e/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c/go.mod h1:CGI5F/G+E5bKwmfYo09AXuVN4dD894kIKUFmVbP2/Fo= google.golang.org/genproto v0.0.0-20221109142239-94d6d90a7d66/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= google.golang.org/genproto v0.0.0-20221114212237-e4508ebdbee1/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= google.golang.org/genproto v0.0.0-20221117204609-8f9c96812029/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= google.golang.org/genproto v0.0.0-20221201164419-0e50fba7f41c/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= google.golang.org/genproto v0.0.0-20221201204527-e3fa12d562f3/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= google.golang.org/genproto v0.0.0-20221202195650-67e5cbc046fd/go.mod h1:cTsE614GARnxrLsqKREzmNYJACSWWpAWdNMwnD7c2BE= google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= google.golang.org/genproto v0.0.0-20230112194545-e10362b5ecf9/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= google.golang.org/genproto v0.0.0-20230113154510-dbe35b8444a5/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= google.golang.org/genproto v0.0.0-20230123190316-2c411cf9d197/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= google.golang.org/genproto v0.0.0-20230124163310-31e0e69b6fc2/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= google.golang.org/genproto v0.0.0-20230125152338-dcaf20b6aeaa/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= google.golang.org/genproto v0.0.0-20230127162408-596548ed4efa/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44/go.mod h1:8B0gmkoRebU8ukX6HP+4wrVQUY1+6PkQ44BSyIlflHA= google.golang.org/genproto v0.0.0-20230222225845-10f96fb3dbec/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw= google.golang.org/genproto v0.0.0-20230223222841-637eb2293923/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw= google.golang.org/genproto v0.0.0-20230303212802-e74f57abe488/go.mod h1:TvhZT5f700eVlTNwND1xoEZQeWTB2RY/65kplwl/bFA= google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s= google.golang.org/genproto v0.0.0-20230320184635-7606e756e683/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s= google.golang.org/genproto v0.0.0-20230323212658-478b75c54725/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= google.golang.org/genproto v0.0.0-20230330154414-c0448cd141ea/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= google.golang.org/genproto v0.0.0-20230331144136-dcfb400f0633/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= google.golang.org/genproto v0.0.0-20230525234025-438c736192d0/go.mod h1:9ExIQyXL5hZrHzQceCwuSYwZZ5QZBazOcprJ5rgs3lY= google.golang.org/genproto v0.0.0-20230526161137-0005af68ea54/go.mod h1:zqTuNwFlFRsw5zIts5VnzLQxSRqh+CGOTVMlYbY0Eyk= google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 h1:1tXaIXCracvtsRxSBsYDiSBN0cuJvM7QYW+MrpIRY78= google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:49MsLSx0oWMOZqcpB3uL8ZOkAh1+TndpJ8ONoCBWiZk= google.golang.org/genproto/googleapis/api v0.0.0-20230525234020-1aefcd67740a/go.mod h1:ts19tUU+Z0ZShN1y3aPyq2+O3d5FUNNgT6FtOzmrNn8= google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY= google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc= google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234015-3fc162c6f38a/go.mod h1:xURIpW9ES5+/GZhnV6beoEtxQrnkRGIfP5VQG2tCBLc= google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww= google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= helm.sh/helm/v3 v3.17.4 h1:GK+vgn9gKCyoH44+f3B5zpA78iH3AK4ywIInDEmmn/g= helm.sh/helm/v3 v3.17.4/go.mod h1:+uJKMH/UiMzZQOALR3XUf3BLIoczI2RKKD6bMhPh4G8= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls= k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k= k8s.io/apiextensions-apiserver v0.32.2 h1:2YMk285jWMk2188V2AERy5yDwBYrjgWYggscghPCvV4= k8s.io/apiextensions-apiserver v0.32.2/go.mod h1:GPwf8sph7YlJT3H6aKUWtd0E+oyShk/YHWQHf/OOgCA= k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U= k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= k8s.io/apiserver v0.32.2 h1:WzyxAu4mvLkQxwD9hGa4ZfExo3yZZaYzoYvvVDlM6vw= k8s.io/apiserver v0.32.2/go.mod h1:PEwREHiHNU2oFdte7BjzA1ZyjWjuckORLIK/wLV5goM= k8s.io/cli-runtime v0.32.2 h1:aKQR4foh9qeyckKRkNXUccP9moxzffyndZAvr+IXMks= k8s.io/cli-runtime v0.32.2/go.mod h1:a/JpeMztz3xDa7GCyyShcwe55p8pbcCVQxvqZnIwXN8= k8s.io/client-go v0.32.3 h1:RKPVltzopkSgHS7aS98QdscAgtgah/+zmpAogooIqVU= k8s.io/client-go v0.32.3/go.mod h1:3v0+3k4IcT9bXTc4V2rt+d2ZPPG700Xy6Oi0Gdl2PaY= k8s.io/component-base v0.32.2 h1:1aUL5Vdmu7qNo4ZsE+569PV5zFatM9hl+lb3dEea2zU= k8s.io/component-base v0.32.2/go.mod h1:PXJ61Vx9Lg+P5mS8TLd7bCIr+eMJRQTyXe8KvkrvJq0= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= k8s.io/kubectl v0.32.2 h1:TAkag6+XfSBgkqK9I7ZvwtF0WVtUAvK8ZqTt+5zi1Us= k8s.io/kubectl v0.32.2/go.mod h1:+h/NQFSPxiDZYX/WZaWw9fwYezGLISP0ud8nQKg+3g8= k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU= k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= knative.dev/pkg v0.0.0-20241026180704-25f6002b00f3 h1:uUSDGlOIkdPT4svjlhi+JEnP2Ufw7AM/F5QDYiEL02U= knative.dev/pkg v0.0.0-20241026180704-25f6002b00f3/go.mod h1:FeMbTLlxQqSASwlRCrYEOsZ0OKUgSj52qxhECwYCJsw= lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= modernc.org/cc/v3 v3.36.2/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= modernc.org/cc/v3 v3.36.3/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= modernc.org/ccgo/v3 v3.0.0-20220428102840-41399a37e894/go.mod h1:eI31LL8EwEBKPpNpA4bU1/i+sKOwOrQy8D87zWUcRZc= modernc.org/ccgo/v3 v3.0.0-20220430103911-bc99d88307be/go.mod h1:bwdAnOoaIt8Ax9YdWGjxWsdkPcZyRPHqrOvJxaKAKGw= modernc.org/ccgo/v3 v3.16.4/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= modernc.org/ccgo/v3 v3.16.8/go.mod h1:zNjwkizS+fIFDrDjIAgBSCLkWbJuHF+ar3QRn+Z9aws= modernc.org/ccgo/v3 v3.16.9/go.mod h1:zNMzC9A9xeNUepy6KuZBbugn3c0Mc9TeiJO4lgvkJDo= modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= modernc.org/libc v0.0.0-20220428101251-2d5f3daf273b/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= modernc.org/libc v1.16.0/go.mod h1:N4LD6DBE9cf+Dzf9buBlzVJndKr/iJHG97vGLHYnb5A= modernc.org/libc v1.16.1/go.mod h1:JjJE0eu4yeK7tab2n4S1w8tlWd9MxXLRzheaRnAKymU= modernc.org/libc v1.16.17/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU= modernc.org/libc v1.16.19/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= modernc.org/libc v1.17.0/go.mod h1:XsgLldpP4aWlPlsjqKRdHPqCxCjISdHfM/yeWC5GyW0= modernc.org/libc v1.17.1/go.mod h1:FZ23b+8LjxZs7XtFMbSzL/EhPxNbfZbErxEHc7cbD9s= modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= modernc.org/memory v1.1.1/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= modernc.org/memory v1.2.0/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= modernc.org/memory v1.2.1/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= modernc.org/sqlite v1.18.1/go.mod h1:6ho+Gow7oX5V+OiOQ6Tr4xeqbx13UZ6t+Fw9IRUG4d4= modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw= modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8= oras.land/oras-go v1.2.5 h1:XpYuAwAb0DfQsunIyMfeET92emK8km3W4yEzZvUbsTo= oras.land/oras-go v1.2.5/go.mod h1:PuAwRShRZCsZb7g8Ar3jKKQR/2A/qN+pkYxIOd/FAoo= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/controller-runtime v0.19.3 h1:XO2GvC9OPftRst6xWCpTgBZO04S2cbp0Qqkj8bX1sPw= sigs.k8s.io/controller-runtime v0.19.3/go.mod h1:j4j87DqtsThvwTv5/Tc5NFRyyF/RF0ip4+62tbTSIUM= sigs.k8s.io/gateway-api v1.2.1 h1:fZZ/+RyRb+Y5tGkwxFKuYuSRQHu9dZtbjenblleOLHM= sigs.k8s.io/gateway-api v1.2.1/go.mod h1:EpNfEXNjiYfUJypf0eZ0P5iXA9ekSGWaS1WgPaM42X0= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= sigs.k8s.io/kustomize/api v0.18.0 h1:hTzp67k+3NEVInwz5BHyzc9rGxIauoXferXyjv5lWPo= sigs.k8s.io/kustomize/api v0.18.0/go.mod h1:f8isXnX+8b+SGLHQ6yO4JG1rdkZlvhaCf/uZbLVMb0U= sigs.k8s.io/kustomize/kyaml v0.18.1 h1:WvBo56Wzw3fjS+7vBjN6TeivvpbW9GmRaWZ9CIVmt4E= sigs.k8s.io/kustomize/kyaml v0.18.1/go.mod h1:C3L2BFVU1jgcddNBE1TxuVLgS46TjObMwW5FT9FcjYo= sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA= sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= ================================================ FILE: main.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import "github.com/k8sgpt-ai/k8sgpt/cmd" var ( version = "dev" commit = "HEAD" date = "unknown" ) func main() { cmd.Execute(version, commit, date) } ================================================ FILE: pkg/ai/amazonbedrock.go ================================================ package ai import ( "context" "errors" "fmt" "os" "regexp" "strings" "github.com/k8sgpt-ai/k8sgpt/pkg/ai/bedrock_support" "github.com/aws/aws-sdk-go-v2/aws" awsconfig "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/bedrock" "github.com/aws/aws-sdk-go-v2/service/bedrockruntime" "github.com/aws/smithy-go/middleware" smithyhttp "github.com/aws/smithy-go/transport/http" ) const amazonbedrockAIClientName = "amazonbedrock" // AmazonBedRockClient represents the client for interacting with the Amazon Bedrock service. type AmazonBedRockClient struct { nopCloser client BedrockRuntimeAPI mgmtClient BedrockManagementAPI model *bedrock_support.BedrockModel temperature float32 topP float32 maxTokens int models []bedrock_support.BedrockModel } // AmazonCompletion BedRock support region list US East (N. Virginia),US West (Oregon),Asia Pacific (Singapore),Asia Pacific (Tokyo),Europe (Frankfurt) // https://docs.aws.amazon.com/bedrock/latest/userguide/what-is-bedrock.html#bedrock-regions const BEDROCK_DEFAULT_REGION = "us-east-1" // default use us-east-1 region const ( US_East_1 = "us-east-1" US_West_2 = "us-west-2" AP_Southeast_1 = "ap-southeast-1" AP_Northeast_1 = "ap-northeast-1" EU_Central_1 = "eu-central-1" AP_South_1 = "ap-south-1" US_Gov_West_1 = "us-gov-west-1" US_Gov_East_1 = "us-gov-east-1" ) var BEDROCKER_SUPPORTED_REGION = []string{ US_East_1, US_West_2, AP_Southeast_1, AP_Northeast_1, EU_Central_1, AP_South_1, US_Gov_West_1, US_Gov_East_1, } var defaultModels = []bedrock_support.BedrockModel{ { Name: "anthropic.claude-sonnet-4-20250514-v1:0", Completion: &bedrock_support.CohereMessagesCompletion{}, Response: &bedrock_support.CohereMessagesResponse{}, Config: bedrock_support.BedrockModelConfig{ // sensible defaults MaxTokens: 100, Temperature: 0.5, TopP: 0.9, ModelName: "anthropic.claude-sonnet-4-20250514-v1:0", }, }, { Name: "us.anthropic.claude-sonnet-4-20250514-v1:0", Completion: &bedrock_support.CohereMessagesCompletion{}, Response: &bedrock_support.CohereMessagesResponse{}, Config: bedrock_support.BedrockModelConfig{ // sensible defaults MaxTokens: 100, Temperature: 0.5, TopP: 0.9, ModelName: "us.anthropic.claude-sonnet-4-20250514-v1:0", }, }, { Name: "eu.anthropic.claude-sonnet-4-20250514-v1:0", Completion: &bedrock_support.CohereMessagesCompletion{}, Response: &bedrock_support.CohereMessagesResponse{}, Config: bedrock_support.BedrockModelConfig{ // sensible defaults MaxTokens: 100, Temperature: 0.5, TopP: 0.9, ModelName: "eu.anthropic.claude-sonnet-4-20250514-v1:0", }, }, { Name: "apac.anthropic.claude-sonnet-4-20250514-v1:0", Completion: &bedrock_support.CohereMessagesCompletion{}, Response: &bedrock_support.CohereMessagesResponse{}, Config: bedrock_support.BedrockModelConfig{ // sensible defaults MaxTokens: 100, Temperature: 0.5, TopP: 0.9, ModelName: "apac.anthropic.claude-sonnet-4-20250514-v1:0", }, }, { Name: "us.anthropic.claude-3-7-sonnet-20250219-v1:0", Completion: &bedrock_support.CohereMessagesCompletion{}, Response: &bedrock_support.CohereMessagesResponse{}, Config: bedrock_support.BedrockModelConfig{ // sensible defaults MaxTokens: 100, Temperature: 0.5, TopP: 0.9, ModelName: "us.anthropic.claude-3-7-sonnet-20250219-v1:0", }, }, { Name: "eu.anthropic.claude-3-7-sonnet-20250219-v1:0", Completion: &bedrock_support.CohereMessagesCompletion{}, Response: &bedrock_support.CohereMessagesResponse{}, Config: bedrock_support.BedrockModelConfig{ // sensible defaults MaxTokens: 100, Temperature: 0.5, TopP: 0.9, ModelName: "eu.anthropic.claude-3-7-sonnet-20250219-v1:0", }, }, { Name: "apac.anthropic.claude-3-7-sonnet-20250219-v1:0", Completion: &bedrock_support.CohereMessagesCompletion{}, Response: &bedrock_support.CohereMessagesResponse{}, Config: bedrock_support.BedrockModelConfig{ // sensible defaults MaxTokens: 100, Temperature: 0.5, TopP: 0.9, ModelName: "apac.anthropic.claude-3-7-sonnet-20250219-v1:0", }, }, { Name: "anthropic.claude-3-5-sonnet-20240620-v1:0", Completion: &bedrock_support.CohereMessagesCompletion{}, Response: &bedrock_support.CohereMessagesResponse{}, Config: bedrock_support.BedrockModelConfig{ // sensible defaults MaxTokens: 100, Temperature: 0.5, TopP: 0.9, ModelName: "anthropic.claude-3-5-sonnet-20240620-v1:0", }, }, { Name: "us.anthropic.claude-3-5-sonnet-20241022-v2:0", Completion: &bedrock_support.CohereMessagesCompletion{}, Response: &bedrock_support.CohereMessagesResponse{}, Config: bedrock_support.BedrockModelConfig{ // sensible defaults MaxTokens: 100, Temperature: 0.5, TopP: 0.9, ModelName: "us.anthropic.claude-3-5-sonnet-20241022-v2:0", }, }, { Name: "anthropic.claude-v2", Completion: &bedrock_support.CohereCompletion{}, Response: &bedrock_support.CohereResponse{}, Config: bedrock_support.BedrockModelConfig{ // sensible defaults MaxTokens: 100, Temperature: 0.5, TopP: 0.9, ModelName: "anthropic.claude-v2", }, }, { Name: "anthropic.claude-v1", Completion: &bedrock_support.CohereCompletion{}, Response: &bedrock_support.CohereResponse{}, Config: bedrock_support.BedrockModelConfig{ // sensible defaults MaxTokens: 100, Temperature: 0.5, TopP: 0.9, ModelName: "anthropic.claude-v1", }, }, { Name: "anthropic.claude-instant-v1", Completion: &bedrock_support.CohereCompletion{}, Response: &bedrock_support.CohereResponse{}, Config: bedrock_support.BedrockModelConfig{ // sensible defaults MaxTokens: 100, Temperature: 0.5, TopP: 0.9, ModelName: "anthropic.claude-instant-v1", }, }, { Name: "ai21.j2-ultra-v1", Completion: &bedrock_support.AI21{}, Response: &bedrock_support.AI21Response{}, Config: bedrock_support.BedrockModelConfig{ // sensible defaults MaxTokens: 100, Temperature: 0.5, TopP: 0.9, ModelName: "ai21.j2-ultra-v1", }, }, { Name: "ai21.j2-jumbo-instruct", Completion: &bedrock_support.AI21{}, Response: &bedrock_support.AI21Response{}, Config: bedrock_support.BedrockModelConfig{ // sensible defaults MaxTokens: 100, Temperature: 0.5, TopP: 0.9, ModelName: "ai21.j2-jumbo-instruct", }, }, { Name: "amazon.titan-text-express-v1", Completion: &bedrock_support.AmazonCompletion{}, Response: &bedrock_support.AmazonResponse{}, Config: bedrock_support.BedrockModelConfig{ // sensible defaults MaxTokens: 100, Temperature: 0.5, TopP: 0.9, ModelName: "amazon.titan-text-express-v1", }, }, { Name: "amazon.nova-pro-v1:0", Completion: &bedrock_support.AmazonCompletion{}, Response: &bedrock_support.NovaResponse{}, Config: bedrock_support.BedrockModelConfig{ // sensible defaults // https://docs.aws.amazon.com/nova/latest/userguide/getting-started-api.html MaxTokens: 100, // max of 300k tokens Temperature: 0.5, TopP: 0.9, ModelName: "amazon.nova-pro-v1:0", }, }, { Name: "eu.amazon.nova-pro-v1:0", Completion: &bedrock_support.AmazonCompletion{}, Response: &bedrock_support.NovaResponse{}, Config: bedrock_support.BedrockModelConfig{ // sensible defaults // https://docs.aws.amazon.com/nova/latest/userguide/getting-started-api.html MaxTokens: 100, // max of 300k tokens Temperature: 0.5, TopP: 0.9, ModelName: "eu.amazon.nova-pro-v1:0", }, }, { Name: "us.amazon.nova-pro-v1:0", Completion: &bedrock_support.AmazonCompletion{}, Response: &bedrock_support.NovaResponse{}, Config: bedrock_support.BedrockModelConfig{ // sensible defaults // https://docs.aws.amazon.com/nova/latest/userguide/getting-started-api.html MaxTokens: 100, // max of 300k tokens Temperature: 0.5, TopP: 0.9, ModelName: "us.amazon.nova-pro-v1:0", }, }, { Name: "amazon.nova-lite-v1:0", Completion: &bedrock_support.AmazonCompletion{}, Response: &bedrock_support.NovaResponse{}, Config: bedrock_support.BedrockModelConfig{ // sensible defaults MaxTokens: 100, // max of 300k tokens Temperature: 0.5, TopP: 0.9, ModelName: "amazon.nova-lite-v1:0", }, }, { Name: "eu.amazon.nova-lite-v1:0", Completion: &bedrock_support.AmazonCompletion{}, Response: &bedrock_support.NovaResponse{}, Config: bedrock_support.BedrockModelConfig{ // sensible defaults MaxTokens: 100, // max of 300k tokens Temperature: 0.5, TopP: 0.9, ModelName: "eu.amazon.nova-lite-v1:0", }, }, { Name: "us.amazon.nova-lite-v1:0", Completion: &bedrock_support.AmazonCompletion{}, Response: &bedrock_support.NovaResponse{}, Config: bedrock_support.BedrockModelConfig{ // sensible defaults MaxTokens: 100, // max of 300k tokens Temperature: 0.5, TopP: 0.9, ModelName: "us.amazon.nova-lite-v1:0", }, }, { Name: "anthropic.claude-3-haiku-20240307-v1:0", Completion: &bedrock_support.CohereMessagesCompletion{}, Response: &bedrock_support.CohereMessagesResponse{}, Config: bedrock_support.BedrockModelConfig{ // sensible defaults MaxTokens: 100, Temperature: 0.5, TopP: 0.9, ModelName: "anthropic.claude-3-haiku-20240307-v1:0", }, }, } // NewAmazonBedRockClient creates a new AmazonBedRockClient with the given models func NewAmazonBedRockClient(models []bedrock_support.BedrockModel) *AmazonBedRockClient { if models == nil { models = defaultModels // Use default models if none provided } return &AmazonBedRockClient{ models: models, } } // GetModelOrDefault check config region func GetRegionOrDefault(region string) string { if os.Getenv("AWS_DEFAULT_REGION") != "" { region = os.Getenv("AWS_DEFAULT_REGION") } // Check if the provided model is in the list for _, m := range BEDROCKER_SUPPORTED_REGION { if m == region { return region // Return the provided model } } // Return the default model if the provided model is not in the list return BEDROCK_DEFAULT_REGION } func validateModelArn(model string) bool { var re = regexp.MustCompile(`(?m)^arn:(?P[^:\n]*):bedrock:(?P[^:\n]*):(?P[^:\n]*):(?P(?P[^:\/\n]*)[:\/])?(?P.*)$`) return re.MatchString(model) } func validateInferenceProfileArn(inferenceProfile string) bool { // Support both inference-profile and application-inference-profile formats var re = regexp.MustCompile(`(?m)^arn:(?P[^:\n]*):bedrock:(?P[^:\n]*):(?P[^:\n]*):(?:inference-profile|application-inference-profile)\/(?P.+)$`) return re.MatchString(inferenceProfile) } // Get model from string func (a *AmazonBedRockClient) getModelFromString(model string) (*bedrock_support.BedrockModel, error) { if model == "" { return nil, errors.New("model name cannot be empty") } // Trim spaces from the model name model = strings.TrimSpace(model) // Try to find an exact match first for i := range a.models { if strings.EqualFold(model, a.models[i].Name) || strings.EqualFold(model, a.models[i].Config.ModelName) { // Create a copy to avoid returning a pointer to a loop variable modelCopy := a.models[i] return &modelCopy, nil } } supportedModels := make([]string, len(a.models)) for i, m := range a.models { supportedModels[i] = m.Name } supportedRegions := BEDROCKER_SUPPORTED_REGION // Pretty-print supported models and regions modelList := "" for _, m := range supportedModels { modelList += " - " + m + "\n" } regionList := "" for _, r := range supportedRegions { regionList += " - " + r + "\n" } return nil, fmt.Errorf( "model '%s' not found in supported models.\n\nSupported models:\n%sSupported regions:\n%s", model, modelList, regionList, ) } // Configure configures the AmazonBedRockClient with the provided configuration. func (a *AmazonBedRockClient) Configure(config IAIConfig) error { // Initialize models if not already initialized if a.models == nil { a.models = defaultModels } // Get the model input modelInput := config.GetModel() // Determine the appropriate region to use var region string // Check if the model input is actually an inference profile ARN if validateInferenceProfileArn(modelInput) { // Extract the region from the inference profile ARN arnParts := strings.Split(modelInput, ":") if len(arnParts) >= 4 { region = arnParts[3] } else { return fmt.Errorf("could not extract region from inference profile ARN: %s", modelInput) } } else { // Use the provided region or default region = GetRegionOrDefault(config.GetProviderRegion()) } // Only create AWS clients if they haven't been injected (for testing) if a.client == nil || a.mgmtClient == nil { // Create a new AWS config with the determined region cfg, err := awsconfig.LoadDefaultConfig(context.Background(), awsconfig.WithRegion(region), ) if err != nil { if strings.Contains(err.Error(), "InvalidAccessKeyId") || strings.Contains(err.Error(), "SignatureDoesNotMatch") || strings.Contains(err.Error(), "NoCredentialProviders") { return fmt.Errorf("AWS credentials are invalid or missing. Please check your AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables or AWS config. Details: %v", err) } return fmt.Errorf("failed to load AWS config for region %s: %w", region, err) } // Create clients with the config a.client = bedrockruntime.NewFromConfig(cfg) a.mgmtClient = bedrock.NewFromConfig(cfg) } // Handle model selection based on input type if validateInferenceProfileArn(modelInput) { // Get the inference profile details profile, err := a.getInferenceProfile(context.Background(), modelInput) if err != nil { return fmt.Errorf("failed to get inference profile: %v", err) } // Extract the model ID from the inference profile modelID, err := a.extractModelFromInferenceProfile(profile) if err != nil { return fmt.Errorf("failed to extract model ID from inference profile: %v", err) } // Find the model configuration for the extracted model ID foundModel, err := a.getModelFromString(modelID) if err != nil { // Instead of failing, use a generic config for completion/response // But still warn user return fmt.Errorf("failed to find model configuration for %s: %v", modelID, err) } // Use the found model config for completion/response, but set ModelName to the profile ARN a.model = foundModel a.model.Config.ModelName = modelInput // Mark that we're using an inference profile // (could add a field if needed) } else { // Regular model ID provided foundModel, err := a.getModelFromString(modelInput) if err != nil { return fmt.Errorf("model '%s' is not supported: %v", modelInput, err) } a.model = foundModel a.model.Config.ModelName = foundModel.Config.ModelName } // Set common configuration parameters a.temperature = config.GetTemperature() a.topP = config.GetTopP() a.maxTokens = config.GetMaxTokens() return nil } // getInferenceProfile retrieves the inference profile details from Amazon Bedrock func (a *AmazonBedRockClient) getInferenceProfile(ctx context.Context, inferenceProfileARN string) (*bedrock.GetInferenceProfileOutput, error) { // Extract the profile ID from the ARN // ARN format: arn:aws:bedrock:region:account-id:inference-profile/profile-id // or arn:aws:bedrock:region:account-id:application-inference-profile/profile-id parts := strings.Split(inferenceProfileARN, "/") if len(parts) != 2 { return nil, fmt.Errorf("invalid inference profile ARN format: %s", inferenceProfileARN) } profileID := parts[1] // Create the input for the GetInferenceProfile API call input := &bedrock.GetInferenceProfileInput{ InferenceProfileIdentifier: aws.String(profileID), } // Call the GetInferenceProfile API output, err := a.mgmtClient.GetInferenceProfile(ctx, input) if err != nil { return nil, fmt.Errorf("failed to get inference profile: %w", err) } return output, nil } // extractModelFromInferenceProfile extracts the model ID from the inference profile func (a *AmazonBedRockClient) extractModelFromInferenceProfile(profile *bedrock.GetInferenceProfileOutput) (string, error) { if profile == nil || len(profile.Models) == 0 { return "", fmt.Errorf("inference profile does not contain any models") } // Check if the first model has a non-nil ModelArn if profile.Models[0].ModelArn == nil { return "", fmt.Errorf("model information is missing in inference profile") } // Get the first model ARN from the profile modelARN := aws.ToString(profile.Models[0].ModelArn) if modelARN == "" { return "", fmt.Errorf("model ARN is empty in inference profile") } // Extract the model ID from the ARN // ARN format: arn:aws:bedrock:region::foundation-model/model-id parts := strings.Split(modelARN, "/") if len(parts) != 2 { return "", fmt.Errorf("invalid model ARN format: %s", modelARN) } modelID := parts[1] return modelID, nil } // GetCompletion sends a request to the model for generating completion based on the provided prompt. func (a *AmazonBedRockClient) GetCompletion(ctx context.Context, prompt string) (string, error) { // override config defaults a.model.Config.MaxTokens = a.maxTokens a.model.Config.Temperature = a.temperature a.model.Config.TopP = a.topP supportedModels := make([]string, len(a.models)) for i, m := range a.models { supportedModels[i] = m.Name } // Allow valid inference profile ARNs as supported models if !bedrock_support.IsModelSupported(a.model.Config.ModelName, supportedModels) && !validateInferenceProfileArn(a.model.Config.ModelName) { return "", fmt.Errorf("model '%s' is not supported.\nSupported models:\n%s", a.model.Config.ModelName, func() string { s := "" for _, m := range supportedModels { s += " - " + m + "\n" } return s }()) } body, err := a.model.Completion.GetCompletion(ctx, prompt, a.model.Config) if err != nil { return "", err } // Build the parameters for the model invocation params := &bedrockruntime.InvokeModelInput{ Body: body, ModelId: aws.String(a.model.Config.ModelName), ContentType: aws.String("application/json"), Accept: aws.String("application/json"), } // Detect if the model name is an inference profile ARN and set the header if so var optFns []func(*bedrockruntime.Options) if validateInferenceProfileArn(a.model.Config.ModelName) { inferenceProfileArn := a.model.Config.ModelName optFns = append(optFns, func(options *bedrockruntime.Options) { options.APIOptions = append(options.APIOptions, func(stack *middleware.Stack) error { return stack.Initialize.Add(middleware.InitializeMiddlewareFunc("InferenceProfileHeader", func(ctx context.Context, in middleware.InitializeInput, next middleware.InitializeHandler) (out middleware.InitializeOutput, metadata middleware.Metadata, err error) { req, ok := in.Parameters.(*smithyhttp.Request) if ok { req.Header.Set("X-Amzn-Bedrock-Inference-Profile-ARN", inferenceProfileArn) } return next.HandleInitialize(ctx, in) }), middleware.Before) }) }) } // Invoke the model var resp *bedrockruntime.InvokeModelOutput if len(optFns) > 0 { resp, err = a.client.InvokeModel(ctx, params, optFns...) } else { resp, err = a.client.InvokeModel(ctx, params) } if err != nil { if strings.Contains(err.Error(), "InvalidAccessKeyId") || strings.Contains(err.Error(), "SignatureDoesNotMatch") || strings.Contains(err.Error(), "NoCredentialProviders") { return "", fmt.Errorf("AWS credentials are invalid or missing. Please check your AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables or AWS config. Details: %v", err) } return "", err } // Parse the response return a.model.Response.ParseResponse(resp.Body) } // GetName returns the name of the AmazonBedRockClient. func (a *AmazonBedRockClient) GetName() string { return amazonbedrockAIClientName } ================================================ FILE: pkg/ai/amazonbedrock_mock_test.go ================================================ package ai import ( "context" "testing" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/bedrock" "github.com/aws/aws-sdk-go-v2/service/bedrock/types" "github.com/aws/aws-sdk-go-v2/service/bedrockruntime" "github.com/k8sgpt-ai/k8sgpt/pkg/ai/bedrock_support" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) // Mock for Bedrock Management Client type MockBedrockClient struct { mock.Mock } func (m *MockBedrockClient) GetInferenceProfile(ctx context.Context, params *bedrock.GetInferenceProfileInput, optFns ...func(*bedrock.Options)) (*bedrock.GetInferenceProfileOutput, error) { args := m.Called(ctx, params) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).(*bedrock.GetInferenceProfileOutput), args.Error(1) } // Mock for Bedrock Runtime Client type MockBedrockRuntimeClient struct { mock.Mock } func (m *MockBedrockRuntimeClient) InvokeModel(ctx context.Context, params *bedrockruntime.InvokeModelInput, optFns ...func(*bedrockruntime.Options)) (*bedrockruntime.InvokeModelOutput, error) { args := m.Called(ctx, params) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).(*bedrockruntime.InvokeModelOutput), args.Error(1) } // TestBedrockInferenceProfileARNWithMocks tests the inference profile ARN validation with mocks func TestBedrockInferenceProfileARNWithMocks(t *testing.T) { // Create test models testModels := []bedrock_support.BedrockModel{ { Name: "anthropic.claude-3-5-sonnet-20240620-v1:0", Completion: &bedrock_support.CohereMessagesCompletion{}, Response: &bedrock_support.CohereMessagesResponse{}, Config: bedrock_support.BedrockModelConfig{ MaxTokens: 100, Temperature: 0.5, TopP: 0.9, ModelName: "anthropic.claude-3-5-sonnet-20240620-v1:0", }, }, } // Create a client with test models client := &AmazonBedRockClient{models: testModels} // Create mock clients mockMgmtClient := new(MockBedrockClient) mockRuntimeClient := new(MockBedrockRuntimeClient) // Inject mock clients into the AmazonBedRockClient client.mgmtClient = mockMgmtClient client.client = mockRuntimeClient // Test with a valid inference profile ARN inferenceProfileARN := "arn:aws:bedrock:us-east-1:123456789012:inference-profile/my-profile" // Setup mock response for GetInferenceProfile mockMgmtClient.On("GetInferenceProfile", mock.Anything, &bedrock.GetInferenceProfileInput{ InferenceProfileIdentifier: aws.String("my-profile"), }).Return(&bedrock.GetInferenceProfileOutput{ Models: []types.InferenceProfileModel{ { ModelArn: aws.String("arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-3-5-sonnet-20240620-v1:0"), }, }, }, nil) // Configure the client with the inference profile ARN config := AIProvider{ Model: inferenceProfileARN, ProviderRegion: "us-east-1", } // Test the Configure method with the inference profile ARN err := client.Configure(&config) // Verify that the configuration was successful assert.NoError(t, err) assert.Equal(t, inferenceProfileARN, client.model.Config.ModelName) // Verify that the mock was called mockMgmtClient.AssertExpectations(t) } ================================================ FILE: pkg/ai/amazonbedrock_test.go ================================================ package ai import ( "testing" "github.com/k8sgpt-ai/k8sgpt/pkg/ai/bedrock_support" "github.com/stretchr/testify/assert" ) // Test models for unit testing var testModels = []bedrock_support.BedrockModel{ { Name: "anthropic.claude-3-5-sonnet-20240620-v1:0", Completion: &bedrock_support.CohereMessagesCompletion{}, Response: &bedrock_support.CohereMessagesResponse{}, Config: bedrock_support.BedrockModelConfig{ MaxTokens: 100, Temperature: 0.5, TopP: 0.9, ModelName: "anthropic.claude-3-5-sonnet-20240620-v1:0", }, }, { Name: "anthropic.claude-3-5-sonnet-20241022-v2:0", Completion: &bedrock_support.CohereCompletion{}, Response: &bedrock_support.CohereResponse{}, Config: bedrock_support.BedrockModelConfig{ MaxTokens: 100, Temperature: 0.5, TopP: 0.9, ModelName: "anthropic.claude-3-5-sonnet-20241022-v2:0", }, }, { Name: "anthropic.claude-3-7-sonnet-20250219-v1:0", Completion: &bedrock_support.CohereCompletion{}, Response: &bedrock_support.CohereResponse{}, Config: bedrock_support.BedrockModelConfig{ MaxTokens: 100, Temperature: 0.5, TopP: 0.9, ModelName: "anthropic.claude-3-7-sonnet-20250219-v1:0", }, }, } func TestBedrockModelConfig(t *testing.T) { client := &AmazonBedRockClient{models: testModels} // Should return error for ARN input (no exact match) _, err := client.getModelFromString("arn:aws:bedrock:us-east-1:*:inference-policy/anthropic.claude-3-5-sonnet-20240620-v1:0") assert.NotNil(t, err, "Should return error for ARN input") } func TestBedrockInvalidModel(t *testing.T) { client := &AmazonBedRockClient{models: testModels} // Should return error for invalid model name _, err := client.getModelFromString("arn:aws:s3:us-east-1:*:inference-policy/anthropic.claude-3-5-sonnet-20240620-v1:0") assert.NotNil(t, err, "Should return error for invalid model name") } func TestBedrockInferenceProfileARN(t *testing.T) { // Create a mock client with test models client := &AmazonBedRockClient{models: testModels} // Test with a valid inference profile ARN inferenceProfileARN := "arn:aws:bedrock:us-east-1:123456789012:inference-profile/my-profile" config := AIProvider{ Model: inferenceProfileARN, ProviderRegion: "us-east-1", } // This will fail in a real environment without mocks, but we're just testing the validation logic err := client.Configure(&config) // We expect an error since we can't actually call AWS in tests assert.NotNil(t, err, "Error should not be nil without AWS mocks") // Test with a valid application inference profile ARN appInferenceProfileARN := "arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/my-profile" config = AIProvider{ Model: appInferenceProfileARN, ProviderRegion: "us-east-1", } // This will fail in a real environment without mocks, but we're just testing the validation logic err = client.Configure(&config) // We expect an error since we can't actually call AWS in tests assert.NotNil(t, err, "Error should not be nil without AWS mocks") // Test with an invalid inference profile ARN format invalidARN := "arn:aws:bedrock:us-east-1:123456789012:invalid-resource/my-profile" config = AIProvider{ Model: invalidARN, ProviderRegion: "us-east-1", } err = client.Configure(&config) assert.NotNil(t, err, "Error should not be nil for invalid inference profile ARN format") } func TestBedrockGetCompletionInferenceProfile(t *testing.T) { modelName := "arn:aws:bedrock:us-east-1:*:inference-policy/anthropic.claude-3-5-sonnet-20240620-v1:0" var inferenceModelModels = []bedrock_support.BedrockModel{ { Name: "anthropic.claude-3-5-sonnet-20240620-v1:0", Completion: &bedrock_support.CohereMessagesCompletion{}, Response: &bedrock_support.CohereMessagesResponse{}, Config: bedrock_support.BedrockModelConfig{ MaxTokens: 100, Temperature: 0.5, TopP: 0.9, ModelName: modelName, }, }, } client := &AmazonBedRockClient{models: inferenceModelModels} config := AIProvider{ Model: modelName, } err := client.Configure(&config) assert.Nil(t, err, "Error should be nil") assert.Equal(t, modelName, client.model.Config.ModelName, "Model name should match") } func TestGetModelFromString(t *testing.T) { client := &AmazonBedRockClient{models: testModels} tests := []struct { name string model string wantModel string wantErr bool }{ { name: "exact model name match", model: "anthropic.claude-3-5-sonnet-20240620-v1:0", wantModel: "anthropic.claude-3-5-sonnet-20240620-v1:0", wantErr: false, }, { name: "partial model name match", model: "claude-3-5-sonnet", wantModel: "anthropic.claude-3-5-sonnet-20240620-v1:0", wantErr: true, }, { name: "model name with different version", model: "anthropic.claude-3-5-sonnet-20241022-v2:0", wantModel: "anthropic.claude-3-5-sonnet-20241022-v2:0", wantErr: false, }, { name: "non-existent model", model: "non-existent-model", wantModel: "", wantErr: true, }, { name: "empty model name", model: "", wantModel: "", wantErr: true, }, { name: "model name with extra spaces", model: " anthropic.claude-3-5-sonnet-20240620-v1:0 ", wantModel: "anthropic.claude-3-5-sonnet-20240620-v1:0", wantErr: false, }, { name: "case insensitive match", model: "ANTHROPIC.CLAUDE-3-5-SONNET-20240620-V1:0", wantModel: "anthropic.claude-3-5-sonnet-20240620-v1:0", wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { gotModel, err := client.getModelFromString(tt.model) if (err != nil) != tt.wantErr { t.Errorf("getModelFromString() error = %v, wantErr %v", err, tt.wantErr) return } if !tt.wantErr && gotModel.Name != tt.wantModel { t.Errorf("getModelFromString() = %v, want %v", gotModel.Name, tt.wantModel) } }) } } // TestDefaultModels tests that the client works with default models func TestDefaultModels(t *testing.T) { client := &AmazonBedRockClient{} // Configure should initialize default models err := client.Configure(&AIProvider{ Model: "anthropic.claude-v2", }) assert.NoError(t, err, "Configure should not return an error") assert.NotNil(t, client.models, "Models should be initialized") assert.NotEmpty(t, client.models, "Models should not be empty") // Test finding a default model model, err := client.getModelFromString("anthropic.claude-v2") assert.NoError(t, err, "Should find the model") assert.Equal(t, "anthropic.claude-v2", model.Name, "Should find the correct model") } func TestValidateInferenceProfileArn(t *testing.T) { tests := []struct { name string arn string valid bool }{ { name: "valid inference profile ARN", arn: "arn:aws:bedrock:us-east-1:123456789012:inference-profile/my-profile", valid: true, }, { name: "valid application inference profile ARN", arn: "arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/my-profile", valid: true, }, { name: "invalid service in ARN", arn: "arn:aws:s3:us-east-1:123456789012:inference-profile/my-profile", valid: false, }, { name: "invalid resource type in ARN", arn: "arn:aws:bedrock:us-east-1:123456789012:model/my-profile", valid: false, }, { name: "malformed ARN", arn: "arn:aws:bedrock:us-east-1:inference-profile/my-profile", valid: false, }, { name: "not an ARN", arn: "not-an-arn", valid: false, }, { name: "empty string", arn: "", valid: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := validateInferenceProfileArn(tt.arn) assert.Equal(t, tt.valid, result, "validateInferenceProfileArn() result should match expected") }) } } ================================================ FILE: pkg/ai/amazonsagemaker.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package ai import ( "context" "encoding/json" "fmt" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/sagemakerruntime" ) const amazonsagemakerAIClientName = "amazonsagemaker" type SageMakerAIClient struct { nopCloser client *sagemakerruntime.SageMakerRuntime model string temperature float32 endpoint string topP float32 topK int32 maxTokens int } type Generations []struct { Generation struct { Role string `json:"role"` Content string `json:"content"` } `json:"generation"` } type Request struct { Inputs [][]Message `json:"inputs"` Parameters Parameters `json:"parameters"` } type Message struct { Role string `json:"role"` Content string `json:"content"` } type Parameters struct { MaxNewTokens int `json:"max_new_tokens"` TopP float64 `json:"top_p"` TopK float64 `json:"top_k"` Temperature float64 `json:"temperature"` } func (c *SageMakerAIClient) Configure(config IAIConfig) error { // Create a new AWS session sess := session.Must(session.NewSessionWithOptions(session.Options{ Config: aws.Config{Region: aws.String(config.GetProviderRegion())}, SharedConfigState: session.SharedConfigEnable, })) // Create a new SageMaker runtime client c.client = sagemakerruntime.New(sess) c.model = config.GetModel() c.endpoint = config.GetEndpointName() c.temperature = config.GetTemperature() c.maxTokens = config.GetMaxTokens() c.topP = config.GetTopP() c.topK = config.GetTopK() return nil } func (c *SageMakerAIClient) GetCompletion(_ context.Context, prompt string) (string, error) { // Create a completion request request := Request{ Inputs: [][]Message{ { {Role: "system", Content: "DEFAULT_PROMPT"}, {Role: "user", Content: prompt}, }, }, Parameters: Parameters{ MaxNewTokens: int(c.maxTokens), TopP: float64(c.topP), TopK: float64(c.topK), Temperature: float64(c.temperature), }, } // Convert request to []byte bytesData, err := json.Marshal(request) if err != nil { return "", err } // Create an input object input := &sagemakerruntime.InvokeEndpointInput{ Body: bytesData, EndpointName: aws.String(c.endpoint), ContentType: aws.String("application/json"), // Set the content type as per your model's requirements Accept: aws.String("application/json"), // Set the accept type as per your model's requirements CustomAttributes: aws.String("accept_eula=true"), } // Call the InvokeEndpoint function result, err := c.client.InvokeEndpoint(input) if err != nil { return "", err } // // Define a slice of Generations var generations Generations err = json.Unmarshal([]byte(string(result.Body)), &generations) if err != nil { return "", err } // Check for length of generations if len(generations) != 1 { return "", fmt.Errorf("Expected exactly one generation, but got %d", len(generations)) } // Access the content content := generations[0].Generation.Content return content, nil } func (c *SageMakerAIClient) GetName() string { return amazonsagemakerAIClientName } ================================================ FILE: pkg/ai/azureopenai.go ================================================ package ai import ( "context" "errors" "net/http" "net/url" "github.com/sashabaranov/go-openai" ) const azureAIClientName = "azureopenai" type AzureAIClient struct { nopCloser client *openai.Client model string temperature float32 // organizationId string } func (c *AzureAIClient) Configure(config IAIConfig) error { token := config.GetPassword() baseURL := config.GetBaseURL() engine := config.GetEngine() proxyEndpoint := config.GetProxyEndpoint() defaultConfig := openai.DefaultAzureConfig(token, baseURL) orgId := config.GetOrganizationId() defaultConfig.AzureModelMapperFunc = func(model string) string { // If you use a deployment name different from the model name, you can customize the AzureModelMapperFunc function azureModelMapping := map[string]string{ model: engine, } return azureModelMapping[model] } if proxyEndpoint != "" { proxyUrl, err := url.Parse(proxyEndpoint) if err != nil { return err } transport := &http.Transport{ Proxy: http.ProxyURL(proxyUrl), } defaultConfig.HTTPClient = &http.Client{ Transport: transport, } } if orgId != "" { defaultConfig.OrgID = orgId } client := openai.NewClientWithConfig(defaultConfig) if client == nil { return errors.New("error creating Azure OpenAI client") } c.client = client c.model = config.GetModel() c.temperature = config.GetTemperature() return nil } func (c *AzureAIClient) GetCompletion(ctx context.Context, prompt string) (string, error) { // Create a completion request resp, err := c.client.CreateChatCompletion(ctx, openai.ChatCompletionRequest{ Model: c.model, Messages: []openai.ChatCompletionMessage{ { Role: openai.ChatMessageRoleUser, Content: prompt, }, }, Temperature: c.temperature, }) if err != nil { return "", err } return resp.Choices[0].Message.Content, nil } func (c *AzureAIClient) GetName() string { return azureAIClientName } ================================================ FILE: pkg/ai/bedrock_interfaces.go ================================================ package ai import ( "context" "github.com/aws/aws-sdk-go-v2/service/bedrock" "github.com/aws/aws-sdk-go-v2/service/bedrockruntime" ) // BedrockManagementAPI defines the interface for Bedrock management operations type BedrockManagementAPI interface { GetInferenceProfile(ctx context.Context, params *bedrock.GetInferenceProfileInput, optFns ...func(*bedrock.Options)) (*bedrock.GetInferenceProfileOutput, error) } // BedrockRuntimeAPI defines the interface for Bedrock runtime operations type BedrockRuntimeAPI interface { InvokeModel(ctx context.Context, params *bedrockruntime.InvokeModelInput, optFns ...func(*bedrockruntime.Options)) (*bedrockruntime.InvokeModelOutput, error) } ================================================ FILE: pkg/ai/bedrock_support/completions.go ================================================ package bedrock_support import ( "context" "encoding/json" "fmt" "strings" ) type ICompletion interface { GetCompletion(ctx context.Context, prompt string, modelConfig BedrockModelConfig) ([]byte, error) } type CohereCompletion struct { completion ICompletion } func (a *CohereCompletion) GetCompletion(ctx context.Context, prompt string, modelConfig BedrockModelConfig) ([]byte, error) { request := map[string]interface{}{ "prompt": fmt.Sprintf("\n\nHuman: %s \n\nAssistant:", prompt), "max_tokens_to_sample": modelConfig.MaxTokens, "temperature": modelConfig.Temperature, "top_p": modelConfig.TopP, } body, err := json.Marshal(request) if err != nil { return []byte{}, err } return body, nil } type CohereMessagesCompletion struct { completion ICompletion } func (a *CohereMessagesCompletion) GetCompletion(ctx context.Context, prompt string, modelConfig BedrockModelConfig) ([]byte, error) { request := map[string]interface{}{ "max_tokens": modelConfig.MaxTokens, "temperature": modelConfig.Temperature, "top_p": modelConfig.TopP, "anthropic_version": "bedrock-2023-05-31", // Or another valid version "messages": []map[string]interface{}{ { "role": "user", "content": prompt, }, }, } body, err := json.Marshal(request) if err != nil { return []byte{}, err } return body, nil } type AI21 struct { completion ICompletion } func (a *AI21) GetCompletion(ctx context.Context, prompt string, modelConfig BedrockModelConfig) ([]byte, error) { request := map[string]interface{}{ "prompt": prompt, "maxTokens": modelConfig.MaxTokens, "temperature": modelConfig.Temperature, "topP": modelConfig.TopP, } body, err := json.Marshal(request) if err != nil { return []byte{}, err } return body, nil } type AmazonCompletion struct { completion ICompletion } // Accepts a list of supported model names func IsModelSupported(modelName string, supportedModels []string) bool { for _, supportedModel := range supportedModels { if strings.EqualFold(modelName, supportedModel) { return true } } return false } // Note: The caller should check model support before calling GetCompletion. func (a *AmazonCompletion) GetCompletion(ctx context.Context, prompt string, modelConfig BedrockModelConfig) ([]byte, error) { if a == nil || modelConfig.ModelName == "" { return nil, fmt.Errorf("no model name provided to Bedrock completion") } if strings.Contains(modelConfig.ModelName, "nova") { return a.GetNovaCompletion(ctx, prompt, modelConfig) } else { return a.GetDefaultCompletion(ctx, prompt, modelConfig) } } func (a *AmazonCompletion) GetDefaultCompletion(ctx context.Context, prompt string, modelConfig BedrockModelConfig) ([]byte, error) { request := map[string]interface{}{ "inputText": fmt.Sprintf("\n\nUser: %s", prompt), "textGenerationConfig": map[string]interface{}{ "maxTokenCount": modelConfig.MaxTokens, "temperature": modelConfig.Temperature, "topP": modelConfig.TopP, }, } body, err := json.Marshal(request) if err != nil { return []byte{}, err } return body, nil } func (a *AmazonCompletion) GetNovaCompletion(ctx context.Context, prompt string, modelConfig BedrockModelConfig) ([]byte, error) { request := map[string]interface{}{ "inferenceConfig": map[string]interface{}{ "max_new_tokens": modelConfig.MaxTokens, "temperature": modelConfig.Temperature, "topP": modelConfig.TopP, }, "messages": []map[string]interface{}{ { "role": "user", "content": []map[string]interface{}{ { "text": prompt, }, }, }, }, } body, err := json.Marshal(request) if err != nil { return []byte{}, err } return body, nil } ================================================ FILE: pkg/ai/bedrock_support/completions_test.go ================================================ package bedrock_support import ( "context" "encoding/json" "testing" "github.com/stretchr/testify/assert" ) func TestCohereCompletion_GetCompletion(t *testing.T) { completion := &CohereCompletion{} modelConfig := BedrockModelConfig{ MaxTokens: 100, Temperature: 0.7, TopP: 0.9, } prompt := "Test prompt" body, err := completion.GetCompletion(context.Background(), prompt, modelConfig) assert.NoError(t, err) var request map[string]interface{} err = json.Unmarshal(body, &request) assert.NoError(t, err) assert.Equal(t, "\n\nHuman: Test prompt \n\nAssistant:", request["prompt"]) assert.Equal(t, 100, int(request["max_tokens_to_sample"].(float64))) assert.Equal(t, 0.7, request["temperature"]) assert.Equal(t, 0.9, request["top_p"]) } func TestAI21_GetCompletion(t *testing.T) { completion := &AI21{} modelConfig := BedrockModelConfig{ MaxTokens: 150, Temperature: 0.6, TopP: 0.8, } prompt := "Another test prompt" body, err := completion.GetCompletion(context.Background(), prompt, modelConfig) assert.NoError(t, err) var request map[string]interface{} err = json.Unmarshal(body, &request) assert.NoError(t, err) assert.Equal(t, "Another test prompt", request["prompt"]) assert.Equal(t, 150, int(request["maxTokens"].(float64))) assert.Equal(t, 0.6, request["temperature"]) assert.Equal(t, 0.8, request["topP"]) } func TestAmazonCompletion_GetDefaultCompletion(t *testing.T) { completion := &AmazonCompletion{} modelConfig := BedrockModelConfig{ MaxTokens: 200, Temperature: 0.5, TopP: 0.7, ModelName: "amazon.titan-text-express-v1", } prompt := "Default test prompt" body, err := completion.GetDefaultCompletion(context.Background(), prompt, modelConfig) assert.NoError(t, err) var request map[string]interface{} err = json.Unmarshal(body, &request) assert.NoError(t, err) assert.Equal(t, "\n\nUser: Default test prompt", request["inputText"]) textConfig := request["textGenerationConfig"].(map[string]interface{}) assert.Equal(t, 200, int(textConfig["maxTokenCount"].(float64))) assert.Equal(t, 0.5, textConfig["temperature"]) assert.Equal(t, 0.7, textConfig["topP"]) } func TestAmazonCompletion_GetNovaCompletion(t *testing.T) { completion := &AmazonCompletion{} modelConfig := BedrockModelConfig{ MaxTokens: 250, Temperature: 0.4, TopP: 0.6, ModelName: "amazon.nova-pro-v1:0", } prompt := "Nova test prompt" body, err := completion.GetNovaCompletion(context.Background(), prompt, modelConfig) assert.NoError(t, err) var request map[string]interface{} err = json.Unmarshal(body, &request) assert.NoError(t, err) inferenceConfig := request["inferenceConfig"].(map[string]interface{}) assert.Equal(t, 250, int(inferenceConfig["max_new_tokens"].(float64))) assert.Equal(t, 0.4, inferenceConfig["temperature"]) assert.Equal(t, 0.6, inferenceConfig["topP"]) messages := request["messages"].([]interface{}) message := messages[0].(map[string]interface{}) content := message["content"].([]interface{}) contentMap := content[0].(map[string]interface{}) assert.Equal(t, "Nova test prompt", contentMap["text"]) } func TestAmazonCompletion_GetCompletion_Nova(t *testing.T) { completion := &AmazonCompletion{} modelConfig := BedrockModelConfig{ MaxTokens: 250, Temperature: 0.4, TopP: 0.6, ModelName: "amazon.nova-pro-v1:0", } prompt := "Nova test prompt" body, err := completion.GetCompletion(context.Background(), prompt, modelConfig) assert.NoError(t, err) var request map[string]interface{} err = json.Unmarshal(body, &request) assert.NoError(t, err) inferenceConfig := request["inferenceConfig"].(map[string]interface{}) assert.Equal(t, 250, int(inferenceConfig["max_new_tokens"].(float64))) assert.Equal(t, 0.4, inferenceConfig["temperature"]) assert.Equal(t, 0.6, inferenceConfig["topP"]) messages := request["messages"].([]interface{}) message := messages[0].(map[string]interface{}) content := message["content"].([]interface{}) contentMap := content[0].(map[string]interface{}) assert.Equal(t, "Nova test prompt", contentMap["text"]) } func TestAmazonCompletion_GetCompletion_Default(t *testing.T) { completion := &AmazonCompletion{} modelConfig := BedrockModelConfig{ MaxTokens: 200, Temperature: 0.5, TopP: 0.7, ModelName: "amazon.titan-text-express-v1", } prompt := "Default test prompt" body, err := completion.GetCompletion(context.Background(), prompt, modelConfig) assert.NoError(t, err) var request map[string]interface{} err = json.Unmarshal(body, &request) assert.NoError(t, err) assert.Equal(t, "\n\nUser: Default test prompt", request["inputText"]) textConfig := request["textGenerationConfig"].(map[string]interface{}) assert.Equal(t, 200, int(textConfig["maxTokenCount"].(float64))) assert.Equal(t, 0.5, textConfig["temperature"]) assert.Equal(t, 0.7, textConfig["topP"]) } func TestAmazonCompletion_GetCompletion_Inference_Profile(t *testing.T) { completion := &AmazonCompletion{} modelConfig := BedrockModelConfig{ MaxTokens: 200, Temperature: 0.5, TopP: 0.7, ModelName: "arn:aws:bedrock:us-east-1:*:inference-policy/anthropic.claude-3-5-sonnet-20240620-v1:0", } prompt := "Test prompt" _, err := completion.GetCompletion(context.Background(), prompt, modelConfig) assert.NoError(t, err) } func TestIsModelSupported(t *testing.T) { supported := []string{ "anthropic.claude-v2", "anthropic.claude-v1", } assert.True(t, IsModelSupported("anthropic.claude-v2", supported)) assert.False(t, IsModelSupported("unsupported-model", supported)) } ================================================ FILE: pkg/ai/bedrock_support/model.go ================================================ package bedrock_support type BedrockModelConfig struct { MaxTokens int Temperature float32 TopP float32 ModelName string } type BedrockModel struct { Name string Completion ICompletion Response IResponse Config BedrockModelConfig } ================================================ FILE: pkg/ai/bedrock_support/model_test.go ================================================ package bedrock_support import ( "context" "testing" "github.com/stretchr/testify/assert" ) func TestBedrockModelConfig(t *testing.T) { config := BedrockModelConfig{ MaxTokens: 100, Temperature: 0.7, TopP: 0.9, ModelName: "test-model", } assert.Equal(t, 100, config.MaxTokens) assert.Equal(t, float32(0.7), config.Temperature) assert.Equal(t, float32(0.9), config.TopP) assert.Equal(t, "test-model", config.ModelName) } func TestBedrockModel(t *testing.T) { completion := &MockCompletion{} response := &MockResponse{} config := BedrockModelConfig{ MaxTokens: 100, Temperature: 0.7, TopP: 0.9, ModelName: "test-model", } model := BedrockModel{ Name: "Test Model", Completion: completion, Response: response, Config: config, } assert.Equal(t, "Test Model", model.Name) assert.Equal(t, completion, model.Completion) assert.Equal(t, response, model.Response) assert.Equal(t, config, model.Config) } // MockCompletion is a mock implementation of the ICompletion interface type MockCompletion struct{} func (m *MockCompletion) GetCompletion(ctx context.Context, prompt string, config BedrockModelConfig) ([]byte, error) { return []byte(`{"prompt": "mock prompt"}`), nil } // MockResponse is a mock implementation of the IResponse interface type MockResponse struct{} func (m *MockResponse) ParseResponse(body []byte) (string, error) { return "mock response", nil } ================================================ FILE: pkg/ai/bedrock_support/responses.go ================================================ package bedrock_support import ( "encoding/json" ) type IResponse interface { ParseResponse(rawResponse []byte) (string, error) } type CohereMessagesResponse struct { response IResponse } func (a *CohereMessagesResponse) ParseResponse(rawResponse []byte) (string, error) { type InvokeModelResponseBody struct { ID string `json:"id"` Type string `json:"type"` Role string `json:"role"` Model string `json:"model"` Content []struct { Type string `json:"type"` Text string `json:"text"` } `json:"content"` StopReason string `json:"stop_reason"` StopSequence interface{} `json:"stop_sequence"` // Could be null Usage struct { InputTokens int `json:"input_tokens"` OutputTokens int `json:"output_tokens"` } `json:"usage"` } output := &InvokeModelResponseBody{} err := json.Unmarshal(rawResponse, output) if err != nil { return "", err } // Extract the text content from the Content array var resultText string for _, content := range output.Content { if content.Type == "text" { resultText += content.Text } } return resultText, nil } type CohereResponse struct { response IResponse } func (a *CohereResponse) ParseResponse(rawResponse []byte) (string, error) { type InvokeModelResponseBody struct { Completion string `json:"completion"` Stop_reason string `json:"stop_reason"` } output := &InvokeModelResponseBody{} err := json.Unmarshal(rawResponse, output) if err != nil { return "", err } return output.Completion, nil } type AI21Response struct { response IResponse } func (a *AI21Response) ParseResponse(rawResponse []byte) (string, error) { type Data struct { Text string `json:"text"` } type Completion struct { Data Data `json:"data"` } type InvokeModelResponseBody struct { Completions []Completion `json:"completions"` } output := &InvokeModelResponseBody{} err := json.Unmarshal(rawResponse, output) if err != nil { return "", err } return output.Completions[0].Data.Text, nil } type AmazonResponse struct { response IResponse } type NovaResponse struct { response NResponse } type NResponse interface { ParseResponse(rawResponse []byte) (string, error) } func (a *AmazonResponse) ParseResponse(rawResponse []byte) (string, error) { type Result struct { TokenCount int `json:"tokenCount"` OutputText string `json:"outputText"` CompletionReason string `json:"completionReason"` } type InvokeModelResponseBody struct { InputTextTokenCount int `json:"inputTextTokenCount"` Results []Result `json:"results"` } output := &InvokeModelResponseBody{} err := json.Unmarshal(rawResponse, output) if err != nil { return "", err } return output.Results[0].OutputText, nil } func (a *NovaResponse) ParseResponse(rawResponse []byte) (string, error) { type Content struct { Text string `json:"text"` } type Message struct { Role string `json:"role"` Content []Content `json:"content"` } type UsageDetails struct { InputTokens int `json:"inputTokens"` OutputTokens int `json:"outputTokens"` TotalTokens int `json:"totalTokens"` CacheReadInputTokenCount int `json:"cacheReadInputTokenCount"` CacheWriteInputTokenCount int `json:"cacheWriteInputTokenCount,omitempty"` } type AmazonNovaResponse struct { Output struct { Message Message `json:"message"` } `json:"output"` StopReason string `json:"stopReason"` Usage UsageDetails `json:"usage"` } response := &AmazonNovaResponse{} err := json.Unmarshal(rawResponse, response) if err != nil { return "", err } if len(response.Output.Message.Content) > 0 { return response.Output.Message.Content[0].Text, nil } return "", nil } ================================================ FILE: pkg/ai/bedrock_support/responses_test.go ================================================ package bedrock_support import ( "testing" "github.com/stretchr/testify/assert" ) func TestCohereResponse_ParseResponse(t *testing.T) { response := &CohereResponse{} rawResponse := []byte(`{"completion": "Test completion", "stop_reason": "max_tokens"}`) result, err := response.ParseResponse(rawResponse) assert.NoError(t, err) assert.Equal(t, "Test completion", result) invalidResponse := []byte(`{"completion": "Test completion", "invalid_json":]`) _, err = response.ParseResponse(invalidResponse) assert.Error(t, err) } func TestAI21Response_ParseResponse(t *testing.T) { response := &AI21Response{} rawResponse := []byte(`{"completions": [{"data": {"text": "AI21 test"}}], "id": "123"}`) result, err := response.ParseResponse(rawResponse) assert.NoError(t, err) assert.Equal(t, "AI21 test", result) invalidResponse := []byte(`{"completions": [{"data": {"text": "AI21 test"}}, "invalid_json":]`) _, err = response.ParseResponse(invalidResponse) assert.Error(t, err) } func TestAmazonResponse_ParseResponse(t *testing.T) { response := &AmazonResponse{} rawResponse := []byte(`{"inputTextTokenCount": 10, "results": [{"tokenCount": 20, "outputText": "Amazon test", "completionReason": "stop"}]}`) result, err := response.ParseResponse(rawResponse) assert.NoError(t, err) assert.Equal(t, "Amazon test", result) invalidResponse := []byte(`{"inputTextTokenCount": 10, "results": [{"tokenCount": 20, "outputText": "Amazon test", "invalid_json":]`) _, err = response.ParseResponse(invalidResponse) assert.Error(t, err) } func TestNovaResponse_ParseResponse(t *testing.T) { response := &NovaResponse{} rawResponse := []byte(`{"output": {"message": {"content": [{"text": "Nova test"}]}}, "stopReason": "stop", "usage": {"inputTokens": 10, "outputTokens": 20, "totalTokens": 30, "cacheReadInputTokenCount": 5}}`) result, err := response.ParseResponse(rawResponse) assert.NoError(t, err) assert.Equal(t, "Nova test", result) rawResponseEmptyContent := []byte(`{"output": {"message": {"content": []}}, "stopReason": "stop", "usage": {"inputTokens": 10, "outputTokens": 20, "totalTokens": 30, "cacheReadInputTokenCount": 5}}`) resultEmptyContent, errEmptyContent := response.ParseResponse(rawResponseEmptyContent) assert.NoError(t, errEmptyContent) assert.Equal(t, "", resultEmptyContent) invalidResponse := []byte(`{"output": {"message": {"content": [{"text": "Nova test"}}, "invalid_json":]`) _, err = response.ParseResponse(invalidResponse) assert.Error(t, err) } ================================================ FILE: pkg/ai/cohere.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package ai import ( "context" "errors" api "github.com/cohere-ai/cohere-go/v2" cohere "github.com/cohere-ai/cohere-go/v2/client" "github.com/cohere-ai/cohere-go/v2/option" ) const cohereAIClientName = "cohere" type CohereClient struct { nopCloser client *cohere.Client model string temperature float32 maxTokens int } func (c *CohereClient) Configure(config IAIConfig) error { token := config.GetPassword() opts := []option.RequestOption{ cohere.WithToken(token), } baseURL := config.GetBaseURL() if baseURL != "" { opts = append(opts, cohere.WithBaseURL(baseURL)) } client := cohere.NewClient(opts...) if client == nil { return errors.New("error creating Cohere client") } c.client = client c.model = config.GetModel() c.temperature = config.GetTemperature() c.maxTokens = config.GetMaxTokens() return nil } func (c *CohereClient) GetCompletion(ctx context.Context, prompt string) (string, error) { // Create a completion request response, err := c.client.Chat(ctx, &api.ChatRequest{ Message: prompt, Model: &c.model, K: api.Int(0), Preamble: api.String(""), Temperature: api.Float64(float64(c.temperature)), RawPrompting: api.Bool(false), MaxTokens: api.Int(c.maxTokens), }) if err != nil { return "", err } return response.Text, nil } func (c *CohereClient) GetName() string { return cohereAIClientName } ================================================ FILE: pkg/ai/customrest.go ================================================ package ai import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "net/url" "strings" "time" ) const CustomRestClientName = "customrest" type CustomRestClient struct { nopCloser client *http.Client base *url.URL token string model string temperature float32 topP float32 topK int32 } type CustomRestRequest struct { Model string `json:"model"` // Prompt is the textual prompt to send to the model. Prompt string `json:"prompt"` // Options lists model-specific options. For example, temperature can be // set through this field, if the model supports it. Options map[string]interface{} `json:"options"` } type CustomRestResponse struct { // Model is the model name that generated the response. Model string `json:"model"` // CreatedAt is the timestamp of the response. CreatedAt time.Time `json:"created_at"` // Response is the textual response itself. Response string `json:"response"` } func (c *CustomRestClient) Configure(config IAIConfig) error { baseURL := config.GetBaseURL() if baseURL == "" { baseURL = defaultBaseURL } c.token = config.GetPassword() baseClientURL, err := url.Parse(baseURL) if err != nil { return err } c.base = baseClientURL proxyEndpoint := config.GetProxyEndpoint() c.client = http.DefaultClient if proxyEndpoint != "" { proxyUrl, err := url.Parse(proxyEndpoint) if err != nil { return err } transport := &http.Transport{ Proxy: http.ProxyURL(proxyUrl), } c.client = &http.Client{ Transport: transport, } } c.model = config.GetModel() if c.model == "" { c.model = defaultModel } c.temperature = config.GetTemperature() c.topP = config.GetTopP() c.topK = config.GetTopK() return nil } func (c *CustomRestClient) GetCompletion(ctx context.Context, prompt string) (string, error) { var promptDetail struct { Language string `json:"language,omitempty"` Message string `json:"message"` Prompt string `json:"prompt,omitempty"` } prompt = strings.NewReplacer("\n", "\\n", "\t", "\\t").Replace(prompt) if err := json.Unmarshal([]byte(prompt), &promptDetail); err != nil { return "", err } generateRequest := &CustomRestRequest{ Model: c.model, Prompt: promptDetail.Prompt, Options: map[string]interface{}{ "temperature": c.temperature, "top_p": c.topP, "top_k": c.topK, "message": promptDetail.Message, "language": promptDetail.Language, }, } requestBody, err := json.Marshal(generateRequest) if err != nil { return "", err } request, err := http.NewRequestWithContext(ctx, http.MethodPost, c.base.String(), bytes.NewBuffer(requestBody)) if err != nil { return "", err } if c.token != "" { request.Header.Set("Authorization", "Bearer "+c.token) } request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/x-ndjson") response, err := c.client.Do(request) if err != nil { return "", err } defer response.Body.Close() responseBody, err := io.ReadAll(response.Body) if err != nil { return "", fmt.Errorf("could not read response body: %w", err) } if response.StatusCode >= http.StatusBadRequest { return "", fmt.Errorf("Request Error, StatusCode: %d, ErrorMessage: %s", response.StatusCode, responseBody) } var result CustomRestResponse if err := json.Unmarshal(responseBody, &result); err != nil { return "", err } return result.Response, nil } func (c *CustomRestClient) GetName() string { return CustomRestClientName } ================================================ FILE: pkg/ai/factory.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package ai import ( "github.com/spf13/viper" ) // AIClientFactory is an interface for creating AI clients type AIClientFactory interface { NewClient(provider string) IAI } // DefaultAIClientFactory is the default implementation of AIClientFactory type DefaultAIClientFactory struct{} // NewClient creates a new AI client using the default implementation func (f *DefaultAIClientFactory) NewClient(provider string) IAI { return NewClient(provider) } // ConfigProvider is an interface for accessing configuration type ConfigProvider interface { UnmarshalKey(key string, rawVal interface{}) error } // ViperConfigProvider is the default implementation of ConfigProvider using Viper type ViperConfigProvider struct{} // UnmarshalKey unmarshals a key from the configuration using Viper func (p *ViperConfigProvider) UnmarshalKey(key string, rawVal interface{}) error { return viper.UnmarshalKey(key, rawVal) } // Default instances to be used var ( DefaultClientFactory = &DefaultAIClientFactory{} DefaultConfigProvider = &ViperConfigProvider{} ) // For testing - these variables can be overridden in tests var ( testAIClientFactory AIClientFactory = nil testConfigProvider ConfigProvider = nil ) // GetAIClientFactory returns the test factory if set, otherwise the default func GetAIClientFactory() AIClientFactory { if testAIClientFactory != nil { return testAIClientFactory } return DefaultClientFactory } // GetConfigProvider returns the test provider if set, otherwise the default func GetConfigProvider() ConfigProvider { if testConfigProvider != nil { return testConfigProvider } return DefaultConfigProvider } // For testing - set the test implementations func SetTestAIClientFactory(factory AIClientFactory) { testAIClientFactory = factory } func SetTestConfigProvider(provider ConfigProvider) { testConfigProvider = provider } // Reset test implementations func ResetTestImplementations() { testAIClientFactory = nil testConfigProvider = nil } ================================================ FILE: pkg/ai/googlegenai.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package ai import ( "context" "errors" "fmt" "github.com/fatih/color" "github.com/google/generative-ai-go/genai" "google.golang.org/api/option" ) const googleAIClientName = "google" type GoogleGenAIClient struct { client *genai.Client model string temperature float32 topP float32 topK int32 maxTokens int } func (c *GoogleGenAIClient) Configure(config IAIConfig) error { ctx := context.Background() // Access your API key as an environment variable (see "Set up your API key" above) token := config.GetPassword() authOption := option.WithAPIKey(token) if token[0] == '{' { authOption = option.WithCredentialsJSON([]byte(token)) } client, err := genai.NewClient(ctx, authOption) if err != nil { return fmt.Errorf("creating genai Google SDK client: %w", err) } c.client = client c.model = config.GetModel() c.temperature = config.GetTemperature() c.topP = config.GetTopP() c.topK = config.GetTopK() c.maxTokens = config.GetMaxTokens() return nil } func (c *GoogleGenAIClient) GetCompletion(ctx context.Context, prompt string) (string, error) { // Available models are at https://ai.google.dev/models e.g.gemini-pro. model := c.client.GenerativeModel(c.model) model.SetTemperature(c.temperature) model.SetTopP(c.topP) model.SetTopK(c.topK) model.SetMaxOutputTokens(int32(c.maxTokens)) // Google AI SDK is capable of different inputs than just text, for now set explicit text prompt type. // Similarly, we could stream the response. For now k8sgpt does not support streaming. resp, err := model.GenerateContent(ctx, genai.Text(prompt)) if err != nil { return "", err } if len(resp.Candidates) == 0 { if resp.PromptFeedback.BlockReason == genai.BlockReasonSafety { for _, r := range resp.PromptFeedback.SafetyRatings { if !r.Blocked { continue } return "", fmt.Errorf("completion blocked due to %v with probability %v", r.Category.String(), r.Probability.String()) } } return "", errors.New("no completion returned; unknown reason") } // Format output. // TODO(bwplotka): Provider richer output in certain cases e.g. suddenly finished // completion based on finish reasons or safety rankings. got := resp.Candidates[0] var output string for _, part := range got.Content.Parts { switch o := part.(type) { case genai.Text: output += string(o) output += "\n" default: color.Yellow("found unsupported AI response part of type %T; ignoring", part) } } if got.CitationMetadata != nil && len(got.CitationMetadata.CitationSources) > 0 { output += "Citations:\n" for _, source := range got.CitationMetadata.CitationSources { // TODO(bwplotka): Give details around what exactly words could be attributed to the citation. output += fmt.Sprintf("* %s, %s\n", *source.URI, source.License) } } return output, nil } func (c *GoogleGenAIClient) GetName() string { return googleAIClientName } func (c *GoogleGenAIClient) Close() { if err := c.client.Close(); err != nil { color.Red("googleai client close error: %v", err) } } ================================================ FILE: pkg/ai/googlevertexai.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package ai import ( "context" "errors" "fmt" "cloud.google.com/go/vertexai/genai" "github.com/fatih/color" ) const googleVertexAIClientName = "googlevertexai" type GoogleVertexAIClient struct { client *genai.Client model string temperature float32 topP float32 topK int32 maxTokens int } // Vertex AI Gemini supported Regions // https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/gemini const VERTEXAI_DEFAULT_REGION = "us-central1" // default use us-east-1 region const ( US_Central_1 = "us-central1" US_West_4 = "us-west4" North_America_Northeast1 = "northamerica-northeast1" US_East_4 = "us-east4" US_West_1 = "us-west1" Asia_Northeast_3 = "asia-northeast3" Asia_Southeast_1 = "asia-southeast1" Asia_Northeast_1 = "asia-northeast1" ) var VERTEXAI_SUPPORTED_REGION = []string{ US_Central_1, US_West_4, North_America_Northeast1, US_East_4, US_West_1, Asia_Northeast_3, Asia_Southeast_1, Asia_Northeast_1, } const ( ModelGeminiProV1 = "gemini-1.0-pro-001" // Retired Model ModelGeminiProV2_5 = "gemini-2.5-pro" // Latest Stable Model ModelGeminiFlashV2_5 = "gemini-2.5-flash" // Latest Stable Model ModelGeminiFlashV2 = "gemini-2.0-flash" // Latest Stable Model ModelGeminiFlashLiteV2 = "gemini-2.0-flash-lite" // Latest Stable Model ModelGeminiProV1_5 = "gemini-1.5-pro-002*" // Legacy Stable Model ModelGeminiFlashV1_5 = "gemini-1.5-flash-002*" // Legacy Stable Model ) var VERTEXAI_MODELS = []string{ ModelGeminiProV2_5, ModelGeminiFlashV2_5, ModelGeminiFlashV2, ModelGeminiFlashLiteV2, ModelGeminiProV1_5, ModelGeminiFlashV1_5, ModelGeminiProV1, } // GetModelOrDefault check config model func GetVertexAIModelOrDefault(model string) string { // Check if the provided model is in the list for _, m := range VERTEXAI_MODELS { if m == model { return model // Return the provided model } } // Return the default model if the provided model is not in the list return VERTEXAI_MODELS[0] } // GetModelOrDefault check config region func GetVertexAIRegionOrDefault(region string) string { // Check if the provided model is in the list for _, m := range VERTEXAI_SUPPORTED_REGION { if m == region { return region // Return the provided model } } // Return the default model if the provided model is not in the list return VERTEXAI_DEFAULT_REGION } func (g *GoogleVertexAIClient) Configure(config IAIConfig) error { ctx := context.Background() // Currently you can access VertexAI either by being authenticated via OAuth or Bearer token so we need to consider both projectId := config.GetProviderId() region := GetVertexAIRegionOrDefault(config.GetProviderRegion()) client, err := genai.NewClient(ctx, projectId, region) if err != nil { return fmt.Errorf("creating genai Google SDK client: %w", err) } g.client = client g.model = GetVertexAIModelOrDefault(config.GetModel()) g.temperature = config.GetTemperature() g.topP = config.GetTopP() g.topK = config.GetTopK() g.maxTokens = config.GetMaxTokens() return nil } func (g *GoogleVertexAIClient) GetCompletion(ctx context.Context, prompt string) (string, error) { model := g.client.GenerativeModel(g.model) model.SetTemperature(g.temperature) model.SetTopP(g.topP) model.SetTopK(g.topK) model.SetMaxOutputTokens(int32(g.maxTokens)) // Google AI SDK is capable of different inputs than just text, for now set explicit text prompt type. // Similarly, we could stream the response. For now k8sgpt does not support streaming. resp, err := model.GenerateContent(ctx, genai.Text(prompt)) if err != nil { return "", err } if len(resp.Candidates) == 0 { if resp.PromptFeedback.BlockReason > 0 { for _, r := range resp.PromptFeedback.SafetyRatings { if !r.Blocked { continue } return "", fmt.Errorf("completion blocked due to %v with probability %v", r.Category.String(), r.Probability.String()) } } return "", errors.New("no completion returned; unknown reason") } // Format output. // TODO(bwplotka): Provider richer output in certain cases e.g. suddenly finished // completion based on finish reasons or safety rankings. got := resp.Candidates[0] var output string for _, part := range got.Content.Parts { switch o := part.(type) { case genai.Text: output += string(o) output += "\n" default: color.Yellow("found unsupported AI response part of type %T; ignoring", part) } } if got.CitationMetadata != nil && len(got.CitationMetadata.Citations) > 0 { output += "Citations:\n" for _, source := range got.CitationMetadata.Citations { // TODO(bwplotka): Give details around what exactly words could be attributed to the citation. output += fmt.Sprintf("* %s, %s\n", source.URI, source.License) } } return output, nil } func (g *GoogleVertexAIClient) GetName() string { return googleVertexAIClientName } func (g *GoogleVertexAIClient) Close() { if err := g.client.Close(); err != nil { color.Red("googleai client close error: %v", err) } } ================================================ FILE: pkg/ai/groq.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package ai import ( "context" "errors" "net/http" "net/url" "github.com/sashabaranov/go-openai" ) const groqAIClientName = "groq" // Default Groq API endpoint (OpenAI-compatible) const groqAPIBaseURL = "https://api.groq.com/openai/v1" type GroqClient struct { nopCloser client *openai.Client model string temperature float32 topP float32 } func (c *GroqClient) Configure(config IAIConfig) error { token := config.GetPassword() defaultConfig := openai.DefaultConfig(token) proxyEndpoint := config.GetProxyEndpoint() baseURL := config.GetBaseURL() if baseURL != "" { defaultConfig.BaseURL = baseURL } else { defaultConfig.BaseURL = groqAPIBaseURL } transport := &http.Transport{} if proxyEndpoint != "" { proxyUrl, err := url.Parse(proxyEndpoint) if err != nil { return err } transport.Proxy = http.ProxyURL(proxyUrl) } customHeaders := config.GetCustomHeaders() defaultConfig.HTTPClient = &http.Client{ Transport: &OpenAIHeaderTransport{ Origin: transport, Headers: customHeaders, }, } client := openai.NewClientWithConfig(defaultConfig) if client == nil { return errors.New("error creating Groq client") } c.client = client c.model = config.GetModel() c.temperature = config.GetTemperature() c.topP = config.GetTopP() return nil } func (c *GroqClient) GetCompletion(ctx context.Context, prompt string) (string, error) { resp, err := c.client.CreateChatCompletion(ctx, openai.ChatCompletionRequest{ Model: c.model, Messages: []openai.ChatCompletionMessage{ { Role: "user", Content: prompt, }, }, Temperature: c.temperature, MaxTokens: maxToken, PresencePenalty: presencePenalty, FrequencyPenalty: frequencyPenalty, TopP: c.topP, }) if err != nil { return "", err } return resp.Choices[0].Message.Content, nil } func (c *GroqClient) GetName() string { return groqAIClientName } ================================================ FILE: pkg/ai/huggingface.go ================================================ package ai import ( "context" "github.com/hupe1980/go-huggingface" "k8s.io/utils/ptr" ) const huggingfaceAIClientName = "huggingface" type HuggingfaceClient struct { nopCloser client *huggingface.InferenceClient model string topP float32 topK int32 temperature float32 maxTokens int } func (c *HuggingfaceClient) Configure(config IAIConfig) error { token := config.GetPassword() client := huggingface.NewInferenceClient(token) c.client = client c.model = config.GetModel() c.topP = config.GetTopP() c.topK = config.GetTopK() c.temperature = config.GetTemperature() if config.GetMaxTokens() > 500 { c.maxTokens = 500 } else { c.maxTokens = config.GetMaxTokens() } return nil } func (c *HuggingfaceClient) GetCompletion(ctx context.Context, prompt string) (string, error) { resp, err := c.client.Conversational(ctx, &huggingface.ConversationalRequest{ Inputs: huggingface.ConverstationalInputs{ Text: prompt, }, Model: c.model, Parameters: huggingface.ConversationalParameters{ TopP: ptr.To[float64](float64(c.topP)), TopK: ptr.To[int](int(c.topK)), Temperature: ptr.To[float64](float64(c.temperature)), MaxLength: &c.maxTokens, }, Options: huggingface.Options{ WaitForModel: ptr.To[bool](true), }, }) if err != nil { return "", err } return resp.GeneratedText, nil } func (c *HuggingfaceClient) GetName() string { return huggingfaceAIClientName } ================================================ FILE: pkg/ai/iai.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package ai import ( "context" "net/http" ) var ( clients = []IAI{ &OpenAIClient{}, &AzureAIClient{}, &LocalAIClient{}, &OllamaClient{}, &NoOpAIClient{}, &CohereClient{}, &AmazonBedRockClient{}, &SageMakerAIClient{}, &GoogleGenAIClient{}, &HuggingfaceClient{}, &GoogleVertexAIClient{}, &OCIGenAIClient{}, &CustomRestClient{}, &IBMWatsonxAIClient{}, &GroqClient{}, } Backends = []string{ openAIClientName, localAIClientName, ollamaClientName, azureAIClientName, cohereAIClientName, amazonbedrockAIClientName, amazonsagemakerAIClientName, googleAIClientName, noopAIClientName, huggingfaceAIClientName, googleVertexAIClientName, ociClientName, CustomRestClientName, ibmWatsonxAIClientName, groqAIClientName, } ) // IAI is an interface all clients (representing backends) share. type IAI interface { // Configure sets up client for given configuration. This is expected to be // executed once per client life-time (e.g. analysis CLI command invocation). Configure(config IAIConfig) error // GetCompletion generates text based on prompt. GetCompletion(ctx context.Context, prompt string) (string, error) // GetName returns name of the backend/client. GetName() string // Close cleans all the resources. No other methods should be used on the // objects after this method is invoked. Close() } type nopCloser struct{} func (nopCloser) Close() {} type IAIConfig interface { GetPassword() string GetModel() string GetBaseURL() string GetProxyEndpoint() string GetEndpointName() string GetEngine() string GetTemperature() float32 GetProviderRegion() string GetTopP() float32 GetTopK() int32 GetMaxTokens() int GetProviderId() string GetCompartmentId() string GetOrganizationId() string GetCustomHeaders() []http.Header } func NewClient(provider string) IAI { for _, c := range clients { if provider == c.GetName() { return c } } // default client return &OpenAIClient{} } type AIConfiguration struct { Providers []AIProvider `mapstructure:"providers"` DefaultProvider string `mapstructure:"defaultprovider"` } type AIProvider struct { Name string `mapstructure:"name"` Model string `mapstructure:"model"` Password string `mapstructure:"password" yaml:"password,omitempty"` BaseURL string `mapstructure:"baseurl" yaml:"baseurl,omitempty"` ProxyEndpoint string `mapstructure:"proxyEndpoint" yaml:"proxyEndpoint,omitempty"` ProxyPort string `mapstructure:"proxyPort" yaml:"proxyPort,omitempty"` EndpointName string `mapstructure:"endpointname" yaml:"endpointname,omitempty"` Engine string `mapstructure:"engine" yaml:"engine,omitempty"` Temperature float32 `mapstructure:"temperature" yaml:"temperature,omitempty"` ProviderRegion string `mapstructure:"providerregion" yaml:"providerregion,omitempty"` ProviderId string `mapstructure:"providerid" yaml:"providerid,omitempty"` CompartmentId string `mapstructure:"compartmentid" yaml:"compartmentid,omitempty"` TopP float32 `mapstructure:"topp" yaml:"topp,omitempty"` TopK int32 `mapstructure:"topk" yaml:"topk,omitempty"` MaxTokens int `mapstructure:"maxtokens" yaml:"maxtokens,omitempty"` OrganizationId string `mapstructure:"organizationid" yaml:"organizationid,omitempty"` CustomHeaders []http.Header `mapstructure:"customHeaders"` } func (p *AIProvider) GetBaseURL() string { return p.BaseURL } func (p *AIProvider) GetProxyEndpoint() string { return p.ProxyEndpoint } func (p *AIProvider) GetEndpointName() string { return p.EndpointName } func (p *AIProvider) GetTopP() float32 { return p.TopP } func (p *AIProvider) GetTopK() int32 { return p.TopK } func (p *AIProvider) GetMaxTokens() int { return p.MaxTokens } func (p *AIProvider) GetPassword() string { return p.Password } func (p *AIProvider) GetModel() string { return p.Model } func (p *AIProvider) GetEngine() string { return p.Engine } func (p *AIProvider) GetTemperature() float32 { return p.Temperature } func (p *AIProvider) GetProviderRegion() string { return p.ProviderRegion } func (p *AIProvider) GetProviderId() string { return p.ProviderId } func (p *AIProvider) GetCompartmentId() string { return p.CompartmentId } func (p *AIProvider) GetOrganizationId() string { return p.OrganizationId } func (p *AIProvider) GetCustomHeaders() []http.Header { return p.CustomHeaders } var passwordlessProviders = []string{"localai", "ollama", "amazonsagemaker", "amazonbedrock", "googlevertexai", "oci", "customrest"} func NeedPassword(backend string) bool { for _, b := range passwordlessProviders { if b == backend { return false } } return true } ================================================ FILE: pkg/ai/interactive/interactive.go ================================================ package interactive import ( "fmt" "strings" "github.com/fatih/color" "github.com/k8sgpt-ai/k8sgpt/pkg/analysis" "github.com/pterm/pterm" ) type INTERACTIVE_STATE int const ( prompt = "Given the following context: " ) const ( E_RUNNING INTERACTIVE_STATE = iota E_EXITED = iota ) type InteractionRunner struct { config *analysis.Analysis State chan INTERACTIVE_STATE contextWindow []byte } func NewInteractionRunner(config *analysis.Analysis, contextWindow []byte) *InteractionRunner { return &InteractionRunner{ config: config, contextWindow: contextWindow, State: make(chan INTERACTIVE_STATE), } } func (a *InteractionRunner) StartInteraction() { a.State <- E_RUNNING pterm.Println("Interactive mode enabled [type exit to close.]") for { query := pterm.DefaultInteractiveTextInput.WithMultiLine(false) queryString, err := query.Show() if err != nil { fmt.Println(err) } if queryString == "" { continue } if strings.Contains(queryString, "exit") { a.State <- E_EXITED continue } pterm.Println() contextWindow := fmt.Sprintf("%s %s %s", prompt, string(a.contextWindow), queryString) response, err := a.config.AIClient.GetCompletion(a.config.Context, contextWindow) if err != nil { color.Red("Error: %v", err) a.State <- E_EXITED continue } pterm.Println(response) } } ================================================ FILE: pkg/ai/localai.go ================================================ package ai const localAIClientName = "localai" type LocalAIClient struct { OpenAIClient } func (a *LocalAIClient) GetName() string { return localAIClientName } ================================================ FILE: pkg/ai/noopai.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package ai import ( "context" ) const noopAIClientName = "noopai" type NoOpAIClient struct { nopCloser } func (c *NoOpAIClient) Configure(_ IAIConfig) error { return nil } func (c *NoOpAIClient) GetCompletion(_ context.Context, prompt string) (string, error) { response := "I am a noop response to the prompt " + prompt return response, nil } func (c *NoOpAIClient) GetName() string { return noopAIClientName } ================================================ FILE: pkg/ai/ocigenai.go ================================================ /* Copyright 2024 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package ai import ( "context" "errors" "fmt" "github.com/oracle/oci-go-sdk/v65/common" "github.com/oracle/oci-go-sdk/v65/generativeai" "github.com/oracle/oci-go-sdk/v65/generativeaiinference" "reflect" ) const ociClientName = "oci" type ociModelVendor string const ( vendorCohere = "cohere" vendorMeta = "meta" ) type OCIGenAIClient struct { nopCloser client *generativeaiinference.GenerativeAiInferenceClient model *generativeai.Model modelID string compartmentId string temperature float32 topP float32 topK int32 maxTokens int } func (c *OCIGenAIClient) GetName() string { return ociClientName } func (c *OCIGenAIClient) Configure(config IAIConfig) error { config.GetEndpointName() c.modelID = config.GetModel() c.temperature = config.GetTemperature() c.topP = config.GetTopP() c.topK = config.GetTopK() c.maxTokens = config.GetMaxTokens() c.compartmentId = config.GetCompartmentId() provider := common.DefaultConfigProvider() client, err := generativeaiinference.NewGenerativeAiInferenceClientWithConfigurationProvider(provider) if err != nil { return err } c.client = &client model, err := c.getModel(provider) if err != nil { return err } c.model = model return nil } func (c *OCIGenAIClient) GetCompletion(ctx context.Context, prompt string) (string, error) { request := c.newChatRequest(prompt) response, err := c.client.Chat(ctx, request) if err != nil { return "", err } if err != nil { return "", err } return extractGeneratedText(response.ChatResponse) } func (c *OCIGenAIClient) newChatRequest(prompt string) generativeaiinference.ChatRequest { return generativeaiinference.ChatRequest{ ChatDetails: generativeaiinference.ChatDetails{ CompartmentId: &c.compartmentId, ServingMode: c.getServingMode(), ChatRequest: c.getChatModelRequest(prompt), }, } } func (c *OCIGenAIClient) getChatModelRequest(prompt string) generativeaiinference.BaseChatRequest { temperatureF64 := float64(c.temperature) topPF64 := float64(c.topP) topK := int(c.topK) switch c.getVendor() { case vendorMeta: messages := []generativeaiinference.Message{ generativeaiinference.UserMessage{ Content: []generativeaiinference.ChatContent{ generativeaiinference.TextContent{ Text: &prompt, }, }, }, } // 0 is invalid for Meta vendor type, instead use -1 to disable topK sampling. if topK == 0 { topK = -1 } return generativeaiinference.GenericChatRequest{ Messages: messages, TopK: &topK, TopP: &topPF64, Temperature: &temperatureF64, MaxTokens: &c.maxTokens, } default: // Default to cohere return generativeaiinference.CohereChatRequest{ Message: &prompt, MaxTokens: &c.maxTokens, Temperature: &temperatureF64, TopK: &topK, TopP: &topPF64, } } } func extractGeneratedText(llmInferenceResponse generativeaiinference.BaseChatResponse) (string, error) { switch response := llmInferenceResponse.(type) { case generativeaiinference.GenericChatResponse: if len(response.Choices) > 0 && len(response.Choices[0].Message.GetContent()) > 0 { if content, ok := response.Choices[0].Message.GetContent()[0].(generativeaiinference.TextContent); ok { return *content.Text, nil } } return "", errors.New("no text found in oci response") case generativeaiinference.CohereChatResponse: return *response.Text, nil default: return "", fmt.Errorf("unknown oci response type: %s", reflect.TypeOf(llmInferenceResponse).Name()) } } func (c *OCIGenAIClient) getServingMode() generativeaiinference.ServingMode { if c.isBaseModel() { return generativeaiinference.OnDemandServingMode{ ModelId: &c.modelID, } } return generativeaiinference.DedicatedServingMode{ EndpointId: &c.modelID, } } func (c *OCIGenAIClient) getModel(provider common.ConfigurationProvider) (*generativeai.Model, error) { client, err := generativeai.NewGenerativeAiClientWithConfigurationProvider(provider) if err != nil { return nil, err } response, err := client.GetModel(context.Background(), generativeai.GetModelRequest{ ModelId: &c.modelID, }) if err != nil { return nil, err } return &response.Model, nil } func (c *OCIGenAIClient) isBaseModel() bool { return c.model != nil && c.model.Type == generativeai.ModelTypeBase } func (c *OCIGenAIClient) getVendor() ociModelVendor { if c.model == nil || c.model.Vendor == nil { return "" } return ociModelVendor(*c.model.Vendor) } ================================================ FILE: pkg/ai/ollama.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package ai import ( "context" "errors" "net/http" "net/url" ollama "github.com/ollama/ollama/api" ) const ollamaClientName = "ollama" type OllamaClient struct { nopCloser client *ollama.Client model string temperature float32 topP float32 } const ( defaultBaseURL = "http://localhost:11434" defaultModel = "llama3" ) func (c *OllamaClient) Configure(config IAIConfig) error { baseURL := config.GetBaseURL() if baseURL == "" { baseURL = defaultBaseURL } baseClientURL, err := url.Parse(baseURL) if err != nil { return err } proxyEndpoint := config.GetProxyEndpoint() httpClient := http.DefaultClient if proxyEndpoint != "" { proxyUrl, err := url.Parse(proxyEndpoint) if err != nil { return err } transport := &http.Transport{ Proxy: http.ProxyURL(proxyUrl), } httpClient = &http.Client{ Transport: transport, } } c.client = ollama.NewClient(baseClientURL, httpClient) if c.client == nil { return errors.New("error creating Ollama client") } c.model = config.GetModel() if c.model == "" { c.model = defaultModel } c.temperature = config.GetTemperature() c.topP = config.GetTopP() return nil } func (c *OllamaClient) GetCompletion(ctx context.Context, prompt string) (string, error) { req := &ollama.GenerateRequest{ Model: c.model, Prompt: prompt, Stream: new(bool), Options: map[string]interface{}{ "temperature": c.temperature, "top_p": c.topP, }, } completion := "" respFunc := func(resp ollama.GenerateResponse) error { completion = resp.Response return nil } err := c.client.Generate(ctx, req, respFunc) if err != nil { return "", err } return completion, nil } func (a *OllamaClient) GetName() string { return ollamaClientName } ================================================ FILE: pkg/ai/openai.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package ai import ( "context" "errors" "net/http" "net/url" "github.com/sashabaranov/go-openai" ) const openAIClientName = "openai" type OpenAIClient struct { nopCloser client *openai.Client model string temperature float32 topP float32 // organizationId string } const ( // OpenAI completion parameters maxToken = 2048 presencePenalty = 0.0 frequencyPenalty = 0.0 ) func (c *OpenAIClient) Configure(config IAIConfig) error { token := config.GetPassword() defaultConfig := openai.DefaultConfig(token) orgId := config.GetOrganizationId() proxyEndpoint := config.GetProxyEndpoint() baseURL := config.GetBaseURL() if baseURL != "" { defaultConfig.BaseURL = baseURL } transport := &http.Transport{} if proxyEndpoint != "" { proxyUrl, err := url.Parse(proxyEndpoint) if err != nil { return err } transport.Proxy = http.ProxyURL(proxyUrl) } if orgId != "" { defaultConfig.OrgID = orgId } customHeaders := config.GetCustomHeaders() defaultConfig.HTTPClient = &http.Client{ Transport: &OpenAIHeaderTransport{ Origin: transport, Headers: customHeaders, }, } client := openai.NewClientWithConfig(defaultConfig) if client == nil { return errors.New("error creating OpenAI client") } c.client = client c.model = config.GetModel() c.temperature = config.GetTemperature() c.topP = config.GetTopP() return nil } func (c *OpenAIClient) GetCompletion(ctx context.Context, prompt string) (string, error) { // Create a completion request resp, err := c.client.CreateChatCompletion(ctx, openai.ChatCompletionRequest{ Model: c.model, Messages: []openai.ChatCompletionMessage{ { Role: "user", Content: prompt, }, }, Temperature: c.temperature, MaxCompletionTokens: maxToken, PresencePenalty: presencePenalty, FrequencyPenalty: frequencyPenalty, TopP: c.topP, }) if err != nil { return "", err } return resp.Choices[0].Message.Content, nil } func (c *OpenAIClient) GetName() string { return openAIClientName } // OpenAIHeaderTransport is an http.RoundTripper that adds the given headers to each request. type OpenAIHeaderTransport struct { Origin http.RoundTripper Headers []http.Header } // RoundTrip implements the http.RoundTripper interface. func (t *OpenAIHeaderTransport) RoundTrip(req *http.Request) (*http.Response, error) { // Clone the request to avoid modifying the original request clonedReq := req.Clone(req.Context()) for _, header := range t.Headers { for key, values := range header { // Possible values per header: RFC 2616 for _, value := range values { clonedReq.Header.Add(key, value) } } } return t.Origin.RoundTrip(clonedReq) } ================================================ FILE: pkg/ai/openai_header_transport_test.go ================================================ package ai import ( "context" "net/http" "net/http/httptest" "testing" "github.com/stretchr/testify/assert" ) // Mock configuration type mockConfig struct { baseURL string } func (m *mockConfig) GetPassword() string { return "" } func (m *mockConfig) GetOrganizationId() string { return "" } func (m *mockConfig) GetProxyEndpoint() string { return "" } func (m *mockConfig) GetBaseURL() string { return m.baseURL } func (m *mockConfig) GetCustomHeaders() []http.Header { return []http.Header{ {"X-Custom-Header-1": []string{"Value1"}}, {"X-Custom-Header-2": []string{"Value2"}}, {"X-Custom-Header-2": []string{"Value3"}}, // Testing multiple values for the same header } } func (m *mockConfig) GetModel() string { return "" } func (m *mockConfig) GetTemperature() float32 { return 0.0 } func (m *mockConfig) GetTopP() float32 { return 0.0 } func (m *mockConfig) GetCompartmentId() string { return "" } func (m *mockConfig) GetTopK() int32 { return 0.0 } func (m *mockConfig) GetMaxTokens() int { return 0 } func (m *mockConfig) GetEndpointName() string { return "" } func (m *mockConfig) GetEngine() string { return "" } func (m *mockConfig) GetProviderId() string { return "" } func (m *mockConfig) GetProviderRegion() string { return "" } func TestOpenAIClient_CustomHeaders(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "Value1", r.Header.Get("X-Custom-Header-1")) assert.ElementsMatch(t, []string{"Value2", "Value3"}, r.Header["X-Custom-Header-2"]) w.WriteHeader(http.StatusOK) // Mock response for openai completion mockResponse := `{"choices": [{"message": {"content": "test"}}]}` n, err := w.Write([]byte(mockResponse)) if err != nil { t.Fatalf("error writing response: %v", err) } if n != len(mockResponse) { t.Fatalf("expected to write %d bytes but wrote %d bytes", len(mockResponse), n) } })) defer server.Close() config := &mockConfig{baseURL: server.URL} client := &OpenAIClient{} err := client.Configure(config) assert.NoError(t, err) // Make a completion request to trigger the headers ctx := context.Background() _, err = client.GetCompletion(ctx, "foo prompt") assert.NoError(t, err) } ================================================ FILE: pkg/ai/prompts.go ================================================ package ai const ( default_prompt = `Simplify the following Kubernetes error message delimited by triple dashes written in --- %s --- language; --- %s ---. Provide the most possible solution in a step by step style in no more than 280 characters. Write the output in the following format: Error: {Explain error here} Solution: {Step by step solution here} ` prom_conf_prompt = `Simplify the following Prometheus error message delimited by triple dashes written in --- %s --- language; --- %s ---. This error came when validating the Prometheus configuration file. Provide step by step instructions to fix, with suggestions, referencing Prometheus documentation if relevant. Write the output in the following format in no more than 300 characters: Error: {Explain error here} Solution: {Step by step solution here} ` prom_relabel_prompt = ` Return your prompt in this language: %s, beginning with The following is a list of the form: job_name: {Prometheus job_name} relabel_configs: {Prometheus relabel_configs} kubernetes_sd_configs: {Prometheus service discovery config} --- %s --- For each job_name, describe the Kubernetes service and pod labels, namespaces, ports, and containers they match. Return the message: Discovered and parsed Prometheus scrape configurations. For targets to be scraped by Prometheus, ensure they are running with at least one of the following label sets: Then for each job, write this format: - Job: {job_name} - Service Labels: - {list of service labels} - Pod Labels: - {list of pod labels} - Namespaces: - {list of namespaces} - Ports: - {list of ports} - Containers: - {list of container names} ` kyverno_prompt = `Simplify the following Kyverno warnings message delimited by triple dashes written in --- %s --- language; --- %s ---. Provide the most probable solution as a kubectl command. Write the output in the following format, for the solution, only show the kubectl command: Error: {Explain error here} Solution: {kubectl command} ` raw_promt = `{"language": "%s","message": "%s","prompt": "%s"}` ) var PromptMap = map[string]string{ "raw": raw_promt, "default": default_prompt, "PrometheusConfigValidate": prom_conf_prompt, "PrometheusConfigRelabelReport": prom_relabel_prompt, "PolicyReport": kyverno_prompt, "ClusterPolicyReport": kyverno_prompt, } ================================================ FILE: pkg/ai/watsonxai.go ================================================ package ai import ( "context" "errors" "fmt" wx "github.com/IBM/watsonx-go/pkg/models" ) const ibmWatsonxAIClientName = "ibmwatsonxai" type IBMWatsonxAIClient struct { nopCloser client *wx.Client model string temperature float32 topP float32 topK int32 maxNewTokens int } const ( modelMetallama = "ibm/granite-13b-chat-v2" maxTokens = 2048 ) func (c *IBMWatsonxAIClient) Configure(config IAIConfig) error { if config.GetModel() == "" { c.model = modelMetallama } else { c.model = config.GetModel() } if config.GetMaxTokens() == 0 { c.maxNewTokens = maxTokens } else { c.maxNewTokens = config.GetMaxTokens() } c.temperature = config.GetTemperature() c.topP = config.GetTopP() c.topK = config.GetTopK() apiKey := config.GetPassword() if apiKey == "" { return errors.New("No watsonx API key provided") } projectId := config.GetProviderId() if projectId == "" { return errors.New("No watsonx project ID provided") } client, err := wx.NewClient( wx.WithWatsonxAPIKey(apiKey), wx.WithWatsonxProjectID(projectId), ) if err != nil { return fmt.Errorf("Failed to create client for testing. Error: %v", err) } c.client = client return nil } func (c *IBMWatsonxAIClient) GetCompletion(ctx context.Context, prompt string) (string, error) { result, err := c.client.GenerateText( c.model, prompt, wx.WithTemperature((float64)(c.temperature)), wx.WithTopP((float64)(c.topP)), wx.WithTopK((uint)(c.topK)), wx.WithMaxNewTokens((uint)(c.maxNewTokens)), ) if err != nil { return "", fmt.Errorf("Expected no error, but got an error: %v", err) } if result.Text == "" { return "", errors.New("Expected a result, but got an empty string") } return result.Text, nil } func (c *IBMWatsonxAIClient) GetName() string { return ibmWatsonxAIClientName } ================================================ FILE: pkg/analysis/analysis.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package analysis import ( "context" "encoding/base64" "encoding/json" "errors" "fmt" "reflect" "strings" "sync" "time" "github.com/fatih/color" openapi_v2 "github.com/google/gnostic/openapiv2" "github.com/k8sgpt-ai/k8sgpt/pkg/ai" "github.com/k8sgpt-ai/k8sgpt/pkg/analyzer" "github.com/k8sgpt-ai/k8sgpt/pkg/cache" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/custom" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" "github.com/k8sgpt-ai/k8sgpt/pkg/util" "github.com/schollz/progressbar/v3" "github.com/spf13/viper" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) type Analysis struct { Context context.Context Filters []string Client *kubernetes.Client Language string AIClient ai.IAI Results []common.Result Errors []string Namespace string LabelSelector string Cache cache.ICache Explain bool MaxConcurrency int AnalysisAIProvider string // The name of the AI Provider used for this analysis WithDoc bool WithStats bool Stats []common.AnalysisStats } type ( AnalysisStatus string AnalysisErrors []string ) const ( StateOK AnalysisStatus = "OK" StateProblemDetected AnalysisStatus = "ProblemDetected" ) type JsonOutput struct { Provider string `json:"provider"` Errors AnalysisErrors `json:"errors"` Status AnalysisStatus `json:"status"` Problems int `json:"problems"` Results []common.Result `json:"results"` } func NewAnalysis( backend string, language string, filters []string, namespace string, labelSelector string, noCache bool, explain bool, maxConcurrency int, withDoc bool, interactiveMode bool, httpHeaders []string, withStats bool, ) (*Analysis, error) { // Get kubernetes client from viper. kubecontext := viper.GetString("kubecontext") kubeconfig := viper.GetString("kubeconfig") verbose := viper.GetBool("verbose") client, err := kubernetes.NewClient(kubecontext, kubeconfig) if verbose { fmt.Println("Debug: Checking kubernetes client initialization.") } if err != nil { return nil, fmt.Errorf("initialising kubernetes client: %w", err) } if verbose { fmt.Printf("Debug: Kubernetes client initialized, server=%s.\n", client.Config.Host) } // Load remote cache if it is configured. cache, err := cache.GetCacheConfiguration() if verbose { fmt.Println("Debug: Checking cache configuration.") } if err != nil { return nil, err } if verbose { fmt.Printf("Debug: Cache configuration loaded, type=%s.\n", cache.GetName()) } if noCache { cache.DisableCache() if verbose { fmt.Println("Debug: Cache disabled.") } } a := &Analysis{ Context: context.Background(), Filters: filters, Client: client, Language: language, Namespace: namespace, LabelSelector: labelSelector, Cache: cache, Explain: explain, MaxConcurrency: maxConcurrency, WithDoc: withDoc, WithStats: withStats, } if verbose { fmt.Print("Debug: Analysis configuration loaded, ") fmt.Printf("filters=%v, language=%s, ", filters, language) if namespace == "" { fmt.Printf("namespace=none, ") } else { fmt.Printf("namespace=%s, ", namespace) } if labelSelector == "" { fmt.Printf("labelSelector=none, ") } else { fmt.Printf("labelSelector=%s, ", labelSelector) } fmt.Printf("explain=%t, maxConcurrency=%d, ", explain, maxConcurrency) fmt.Printf("withDoc=%t, withStats=%t.\n", withDoc, withStats) } if !explain { // Return early if AI use was not requested. return a, nil } var configAI ai.AIConfiguration if verbose { fmt.Println("Debug: Checking AI configuration.") } if err := viper.UnmarshalKey("ai", &configAI); err != nil { return nil, err } if len(configAI.Providers) == 0 { return nil, errors.New("AI provider not specified in configuration. Please run k8sgpt auth") } // Backend string will have high priority than a default provider // Hence, use the default provider only if the backend is not specified by the user. if configAI.DefaultProvider != "" && backend == "" { backend = configAI.DefaultProvider if verbose { fmt.Printf("Debug: Using default AI provider %s.\n", backend) } } if backend == "" { backend = "openai" if verbose { fmt.Printf("Debug: Using default AI provider %s.\n", backend) } } var aiProvider ai.AIProvider for _, provider := range configAI.Providers { if backend == provider.Name { aiProvider = provider break } } if aiProvider.Name == "" { return nil, fmt.Errorf("AI provider %s not specified in configuration. Please run k8sgpt auth", backend) } if verbose { fmt.Printf("Debug: AI configuration loaded, provider=%s, ", backend) fmt.Printf("baseUrl=%s, model=%s.\n", aiProvider.BaseURL, aiProvider.Model) } aiClient := ai.NewClient(aiProvider.Name) customHeaders := util.NewHeaders(httpHeaders) aiProvider.CustomHeaders = customHeaders if verbose { fmt.Println("Debug: Checking AI client initialization.") } if err := aiClient.Configure(&aiProvider); err != nil { return nil, err } if verbose { fmt.Println("Debug: AI client initialized.") } a.AIClient = aiClient a.AnalysisAIProvider = aiProvider.Name return a, nil } func (a *Analysis) CustomAnalyzersAreAvailable() bool { var customAnalyzers []custom.CustomAnalyzer if err := viper.UnmarshalKey("custom_analyzers", &customAnalyzers); err != nil { return false } return len(customAnalyzers) > 0 } func (a *Analysis) RunCustomAnalysis() { // Validate namespace if specified, consistent with built-in filter behavior if a.Namespace != "" && a.Client != nil { _, err := a.Client.Client.CoreV1().Namespaces().Get(a.Context, a.Namespace, metav1.GetOptions{}) if err != nil { a.Errors = append(a.Errors, fmt.Sprintf("namespace %q not found: %s", a.Namespace, err)) return } } var customAnalyzers []custom.CustomAnalyzer if err := viper.UnmarshalKey("custom_analyzers", &customAnalyzers); err != nil { a.Errors = append(a.Errors, err.Error()) return } semaphore := make(chan struct{}, a.MaxConcurrency) var wg sync.WaitGroup var mutex sync.Mutex verbose := viper.GetBool("verbose") if verbose { if len(customAnalyzers) == 0 { fmt.Println("Debug: No custom analyzers found.") } else { cAnalyzerNames := make([]string, len(customAnalyzers)) for i, cAnalyzer := range customAnalyzers { cAnalyzerNames[i] = cAnalyzer.Name } fmt.Printf("Debug: Found custom analyzers %v.\n", cAnalyzerNames) } } for _, cAnalyzer := range customAnalyzers { wg.Add(1) semaphore <- struct{}{} go func(analyzer custom.CustomAnalyzer, wg *sync.WaitGroup, semaphore chan struct{}) { defer wg.Done() canClient, err := custom.NewClient(cAnalyzer.Connection) if err != nil { mutex.Lock() a.Errors = append(a.Errors, fmt.Sprintf("Client creation error for %s analyzer", cAnalyzer.Name)) mutex.Unlock() return } if verbose { fmt.Printf("Debug: %s launched.\n", cAnalyzer.Name) } result, err := canClient.Run() if result.Kind == "" { // for custom analyzer name, we must use a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', //and must start and end with an alphanumeric character (e.g. 'example.com', //regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*') result.Kind = cAnalyzer.Name } if err != nil { mutex.Lock() a.Errors = append(a.Errors, fmt.Sprintf("[%s] %s", cAnalyzer.Name, err)) mutex.Unlock() if verbose { fmt.Printf("Debug: %s completed with errors.\n", cAnalyzer.Name) } } else { mutex.Lock() a.Results = append(a.Results, result) mutex.Unlock() if verbose { fmt.Printf("Debug: %s completed without errors.\n", cAnalyzer.Name) } } <-semaphore }(cAnalyzer, &wg, semaphore) } wg.Wait() } func (a *Analysis) RunAnalysis() { activeFilters := viper.GetStringSlice("active_filters") verbose := viper.GetBool("verbose") coreAnalyzerMap, analyzerMap := analyzer.GetAnalyzerMap() // we get the openapi schema from the server only if required by the flag "with-doc" openapiSchema := &openapi_v2.Document{} if a.WithDoc { var openApiErr error if verbose { fmt.Println("Debug: Fetching Kubernetes docs.") } openapiSchema, openApiErr = a.Client.Client.Discovery().OpenAPISchema() if verbose { fmt.Println("Debug: Checking Kubernetes docs.") } if openApiErr != nil { a.Errors = append(a.Errors, fmt.Sprintf("[KubernetesDoc] %s", openApiErr)) } } analyzerConfig := common.Analyzer{ Client: a.Client, Context: a.Context, Namespace: a.Namespace, LabelSelector: a.LabelSelector, AIClient: a.AIClient, OpenapiSchema: openapiSchema, } // Set a reasonable maximum for concurrency to prevent excessive memory allocation const maxAllowedConcurrency = 100 concurrency := a.MaxConcurrency if concurrency <= 0 { concurrency = 10 // Default value if not set } else if concurrency > maxAllowedConcurrency { concurrency = maxAllowedConcurrency // Cap at a reasonable maximum } semaphore := make(chan struct{}, concurrency) var wg sync.WaitGroup var mutex sync.Mutex // if there are no filters selected and no active_filters then run coreAnalyzer if len(a.Filters) == 0 && len(activeFilters) == 0 { if verbose { fmt.Println("Debug: No filters selected and no active filters found, run all core analyzers.") } for name, analyzer := range coreAnalyzerMap { wg.Add(1) semaphore <- struct{}{} go a.executeAnalyzer(analyzer, name, analyzerConfig, semaphore, &wg, &mutex) } wg.Wait() return } // if the filters flag is specified if len(a.Filters) != 0 { if verbose { fmt.Printf("Debug: Filter flags %v specified, run selected core analyzers.\n", a.Filters) } for _, filter := range a.Filters { if analyzer, ok := analyzerMap[filter]; ok { semaphore <- struct{}{} wg.Add(1) go a.executeAnalyzer(analyzer, filter, analyzerConfig, semaphore, &wg, &mutex) } else { a.Errors = append(a.Errors, fmt.Sprintf("\"%s\" filter does not exist. Please run k8sgpt filters list.", filter)) } } wg.Wait() return } // use active_filters if len(activeFilters) > 0 && verbose { fmt.Printf("Debug: Found active filters %v, run selected core analyzers.\n", activeFilters) } for _, filter := range activeFilters { if analyzer, ok := analyzerMap[filter]; ok { semaphore <- struct{}{} wg.Add(1) go a.executeAnalyzer(analyzer, filter, analyzerConfig, semaphore, &wg, &mutex) } } wg.Wait() } func (a *Analysis) executeAnalyzer(analyzer common.IAnalyzer, filter string, analyzerConfig common.Analyzer, semaphore chan struct{}, wg *sync.WaitGroup, mutex *sync.Mutex) { defer wg.Done() var startTime time.Time var elapsedTime time.Duration // Start the timer if a.WithStats { startTime = time.Now() } // Run the analyzer verbose := viper.GetBool("verbose") if verbose { fmt.Printf("Debug: %s launched.\n", reflect.TypeOf(analyzer).Name()) } results, err := analyzer.Analyze(analyzerConfig) if err != nil { fmt.Println(err) } // Measure the time taken if a.WithStats { elapsedTime = time.Since(startTime) } stat := common.AnalysisStats{ Analyzer: filter, DurationTime: elapsedTime, } mutex.Lock() defer mutex.Unlock() if err != nil { if a.WithStats { a.Stats = append(a.Stats, stat) } a.Errors = append(a.Errors, fmt.Sprintf("[%s] %s", filter, err)) if verbose { fmt.Printf("Debug: %s completed with errors.\n", reflect.TypeOf(analyzer).Name()) } } else { if a.WithStats { a.Stats = append(a.Stats, stat) } a.Results = append(a.Results, results...) if verbose { fmt.Printf("Debug: %s completed without errors.\n", reflect.TypeOf(analyzer).Name()) } } <-semaphore } func (a *Analysis) GetAIResults(output string, anonymize bool) error { if len(a.Results) == 0 { return nil } verbose := viper.GetBool("verbose") if verbose { fmt.Println("Debug: Generating AI analysis.") } var bar *progressbar.ProgressBar if output != "json" { bar = progressbar.Default(int64(len(a.Results))) } for index, analysis := range a.Results { var texts []string if bar != nil && verbose { bar.Describe(fmt.Sprintf("Analyzing %s", analysis.Kind)) } for _, failure := range analysis.Error { if anonymize { for _, s := range failure.Sensitive { failure.Text = util.ReplaceIfMatch(failure.Text, s.Unmasked, s.Masked) } } texts = append(texts, failure.Text) } promptTemplate := ai.PromptMap["default"] // If the resource `Kind` comes from an "integration plugin", // maybe a customized prompt template will be involved. if prompt, ok := ai.PromptMap[analysis.Kind]; ok { promptTemplate = prompt } result, err := a.getAIResultForSanitizedFailures(texts, promptTemplate) if err != nil { // FIXME: can we avoid checking if output is json multiple times? // maybe implement the progress bar better? if output != "json" { _ = bar.Exit() } // Check for exhaustion. if strings.Contains(err.Error(), "status code: 429") { return fmt.Errorf("exhausted API quota for AI provider %s: %v", a.AIClient.GetName(), err) } return fmt.Errorf("failed while calling AI provider %s: %v", a.AIClient.GetName(), err) } if anonymize { for _, failure := range analysis.Error { for _, s := range failure.Sensitive { result = strings.ReplaceAll(result, s.Masked, s.Unmasked) } } } analysis.Details = result if output != "json" { _ = bar.Add(1) } a.Results[index] = analysis } return nil } func (a *Analysis) getAIResultForSanitizedFailures(texts []string, promptTmpl string) (string, error) { inputKey := strings.Join(texts, " ") // Check for cached data. // TODO(bwplotka): This might depend on model too (or even other client configuration pieces), fix it in later PRs. cacheKey := util.GetCacheKey(a.AIClient.GetName(), a.Language, inputKey) if !a.Cache.IsCacheDisabled() && a.Cache.Exists(cacheKey) { response, err := a.Cache.Load(cacheKey) if err != nil { return "", err } if response != "" { output, err := base64.StdEncoding.DecodeString(response) if err == nil { return string(output), nil } color.Red("error decoding cached data; ignoring cache item: %v", err) } } // Process template. prompt := fmt.Sprintf(strings.TrimSpace(promptTmpl), a.Language, inputKey) if a.AIClient.GetName() == ai.CustomRestClientName { // Use proper JSON marshaling to handle special characters in error messages // This fixes issues with quotes, newlines, and other special chars in inputKey customRestPrompt := struct { Language string `json:"language"` Message string `json:"message"` Prompt string `json:"prompt"` }{ Language: a.Language, Message: inputKey, Prompt: prompt, } promptBytes, err := json.Marshal(customRestPrompt) if err != nil { return "", fmt.Errorf("failed to marshal customrest prompt: %w", err) } prompt = string(promptBytes) } response, err := a.AIClient.GetCompletion(a.Context, prompt) if err != nil { return "", err } if err = a.Cache.Store(cacheKey, base64.StdEncoding.EncodeToString([]byte(response))); err != nil { color.Red("error storing value to cache; value won't be cached: %v", err) } return response, nil } func (a *Analysis) Close() { if a.AIClient == nil { return } a.AIClient.Close() } ================================================ FILE: pkg/analysis/analysis_test.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package analysis import ( "context" "encoding/json" "fmt" "reflect" "strings" "testing" "github.com/agiledragon/gomonkey/v2" "github.com/k8sgpt-ai/k8sgpt/pkg/ai" "github.com/k8sgpt-ai/k8sgpt/pkg/analyzer" "github.com/k8sgpt-ai/k8sgpt/pkg/cache" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" "github.com/k8sgpt-ai/k8sgpt/pkg/util" "github.com/magiconair/properties/assert" "github.com/spf13/viper" "github.com/stretchr/testify/require" v1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/fake" "k8s.io/client-go/rest" ) // helper function: get type name of an analyzer func getTypeName(i interface{}) string { return reflect.TypeOf(i).Name() } // helper function: run analysis with filter func analysis_RunAnalysisFilterTester(t *testing.T, filterFlag string) []common.Result { clientset := fake.NewSimpleClientset( &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "default", }, Status: v1.PodStatus{ Phase: v1.PodPending, Conditions: []v1.PodCondition{ { Type: v1.PodScheduled, Reason: "Unschedulable", Message: "0/1 nodes are available: 1 node(s) had taint {node-role.kubernetes.io/master: }, that the pod didn't tolerate.", }, }, }, }, &v1.Endpoints{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "default", Annotations: map[string]string{}, }, }, &v1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "default", Annotations: map[string]string{}, }, Spec: v1.ServiceSpec{ Selector: map[string]string{ "app": "example", }, }, }, &networkingv1.Ingress{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "default", Annotations: map[string]string{}, }, }, ) analysis := Analysis{ Context: context.Background(), Results: []common.Result{}, Namespace: "default", MaxConcurrency: 1, Client: &kubernetes.Client{ Client: clientset, }, WithDoc: true, } if len(filterFlag) > 0 { // `--filter` is explicitly given analysis.Filters = strings.Split(filterFlag, ",") } analysis.RunAnalysis() return analysis.Results } // Test: Filter logic with running different Analyzers func TestAnalysis_RunAnalysisWithFilter(t *testing.T) { var results []common.Result var filterFlag string //1. Neither --filter flag Nor active filter is specified, only the "core analyzers" results = analysis_RunAnalysisFilterTester(t, "") assert.Equal(t, len(results), 3) // all built-in resource will be analyzed //2. When the --filter flag is specified filterFlag = "Pod" // --filter=Pod results = analysis_RunAnalysisFilterTester(t, filterFlag) assert.Equal(t, len(results), 1) assert.Equal(t, results[0].Kind, filterFlag) filterFlag = "Ingress,Pod" // --filter=Ingress,Pod results = analysis_RunAnalysisFilterTester(t, filterFlag) assert.Equal(t, len(results), 2) } // Test: Filter logic with Active Filter func TestAnalysis_RunAnalysisActiveFilter(t *testing.T) { //When the --filter flag is not specified but has actived filter in config var results []common.Result viper.SetDefault("active_filters", "Ingress") results = analysis_RunAnalysisFilterTester(t, "") assert.Equal(t, len(results), 1) viper.SetDefault("active_filters", []string{"Ingress", "Service"}) results = analysis_RunAnalysisFilterTester(t, "") assert.Equal(t, len(results), 2) viper.SetDefault("active_filters", []string{"Ingress", "Service", "Pod"}) results = analysis_RunAnalysisFilterTester(t, "") assert.Equal(t, len(results), 3) // Invalid filter results = analysis_RunAnalysisFilterTester(t, "invalid") assert.Equal(t, len(results), 0) } func TestAnalysis_NoProblemJsonOutput(t *testing.T) { analysis := Analysis{ Results: []common.Result{}, Namespace: "default", } expected := JsonOutput{ Status: StateOK, Problems: 0, Results: []common.Result{}, } gotJson, err := analysis.PrintOutput("json") if err != nil { t.Error(err) } got := JsonOutput{} err = json.Unmarshal(gotJson, &got) if err != nil { t.Error(err) } fmt.Println(got) fmt.Println(expected) require.Equal(t, got, expected) } func TestAnalysis_ProblemJsonOutput(t *testing.T) { analysis := Analysis{ Results: []common.Result{ { Kind: "Deployment", Name: "test-deployment", Error: []common.Failure{ { Text: "test-problem", Sensitive: []common.Sensitive{}, }, }, Details: "test-solution", ParentObject: "parent-resource"}, }, Namespace: "default", } expected := JsonOutput{ Status: StateProblemDetected, Problems: 1, Results: []common.Result{ { Kind: "Deployment", Name: "test-deployment", Error: []common.Failure{ { Text: "test-problem", Sensitive: []common.Sensitive{}, }, }, Details: "test-solution", ParentObject: "parent-resource"}, }, } gotJson, err := analysis.PrintOutput("json") if err != nil { t.Error(err) } got := JsonOutput{} err = json.Unmarshal(gotJson, &got) if err != nil { t.Error(err) } fmt.Println(got) fmt.Println(expected) require.Equal(t, got, expected) } func TestAnalysis_MultipleProblemJsonOutput(t *testing.T) { analysis := Analysis{ Results: []common.Result{ { Kind: "Deployment", Name: "test-deployment", Error: []common.Failure{ { Text: "test-problem", Sensitive: []common.Sensitive{}, }, { Text: "another-test-problem", Sensitive: []common.Sensitive{}, }, }, Details: "test-solution", ParentObject: "parent-resource"}, }, Namespace: "default", } expected := JsonOutput{ Status: StateProblemDetected, Problems: 2, Results: []common.Result{ { Kind: "Deployment", Name: "test-deployment", Error: []common.Failure{ { Text: "test-problem", Sensitive: []common.Sensitive{}, }, { Text: "another-test-problem", Sensitive: []common.Sensitive{}, }, }, Details: "test-solution", ParentObject: "parent-resource"}, }, } gotJson, err := analysis.PrintOutput("json") if err != nil { t.Error(err) } got := JsonOutput{} err = json.Unmarshal(gotJson, &got) if err != nil { t.Error(err) } fmt.Println(got) fmt.Println(expected) require.Equal(t, got, expected) } func TestNewAnalysis(t *testing.T) { disabledCache := cache.New("disabled-cache") disabledCache.DisableCache() aiClient := &ai.NoOpAIClient{} results := []common.Result{ { Kind: "VulnerabilityReport", Error: []common.Failure{ { Text: "This is a custom failure", KubernetesDoc: "test-kubernetes-doc", Sensitive: []common.Sensitive{ { Masked: "masked-error", Unmasked: "unmasked-error", }, }, }, }, }, } tests := []struct { name string a Analysis output string anonymize bool expectedErr string }{ { name: "Empty results", a: Analysis{}, }, { name: "cache disabled", a: Analysis{ AIClient: aiClient, Cache: disabledCache, Results: results, }, }, { name: "output and anonymize both set", a: Analysis{ AIClient: aiClient, Cache: cache.New("test-cache"), Results: results, }, output: "test-output", anonymize: true, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { err := tt.a.GetAIResults(tt.output, tt.anonymize) if tt.expectedErr == "" { require.NoError(t, err) } else { require.ErrorContains(t, err, tt.expectedErr) } }) } } func TestGetAIResultForSanitizedFailures(t *testing.T) { enabledCache := cache.New("enabled-cache") disabledCache := cache.New("disabled-cache") disabledCache.DisableCache() aiClient := &ai.NoOpAIClient{} tests := []struct { name string a Analysis texts []string promptTmpl string expectedOutput string expectedErr string }{ { name: "Cache enabled", a: Analysis{ AIClient: aiClient, Cache: enabledCache, }, texts: []string{"some-data"}, expectedOutput: "I am a noop response to the prompt %!(EXTRA string=, string=some-data)", }, { name: "cache disabled", a: Analysis{ AIClient: aiClient, Cache: disabledCache, Language: "English", }, texts: []string{"test input"}, promptTmpl: "Response in %s: %s", expectedOutput: "I am a noop response to the prompt Response in English: test input", }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { output, err := tt.a.getAIResultForSanitizedFailures(tt.texts, tt.promptTmpl) if tt.expectedErr == "" { require.NoError(t, err) require.Equal(t, tt.expectedOutput, output) } else { require.ErrorContains(t, err, tt.expectedErr) require.Empty(t, output) } }) } } // Test: Verbose output in NewAnalysis with explain=false func TestVerbose_NewAnalysisWithoutExplain(t *testing.T) { // Set viper config. viper.Set("verbose", true) viper.Set("kubecontext", "dummy") viper.Set("kubeconfig", "dummy") // Patch kubernetes.NewClient to return a dummy client. patches := gomonkey.ApplyFunc(kubernetes.NewClient, func(kubecontext, kubeconfig string) (*kubernetes.Client, error) { return &kubernetes.Client{ Config: &rest.Config{Host: "fake-server"}, }, nil }) defer patches.Reset() output := util.CaptureOutput(func() { a, err := NewAnalysis( "", "english", []string{"Pod"}, "default", "", true, false, // explain 10, false, false, []string{}, false, ) require.NoError(t, err) a.Close() }) expectedOutputs := []string{ "Debug: Checking kubernetes client initialization.", "Debug: Kubernetes client initialized, server=fake-server.", "Debug: Checking cache configuration.", "Debug: Cache configuration loaded, type=file.", "Debug: Cache disabled.", "Debug: Analysis configuration loaded, filters=[Pod], language=english, namespace=default, labelSelector=none, explain=false, maxConcurrency=10, withDoc=false, withStats=false.", } for _, expected := range expectedOutputs { if !util.Contains(output, expected) { t.Errorf("Expected output to contain: '%s', but got output: '%s'", expected, output) } } } // Test: Verbose output in NewAnalysis with explain=true func TestVerbose_NewAnalysisWithExplain(t *testing.T) { // Set viper config. viper.Set("verbose", true) viper.Set("kubecontext", "dummy") viper.Set("kubeconfig", "dummy") // Set a dummy AI configuration. dummyAIConfig := map[string]interface{}{ "defaultProvider": "dummy", "providers": []map[string]interface{}{ { "name": "dummy", "baseUrl": "http://dummy", "model": "dummy-model", "customHeaders": map[string]string{}, }, }, } viper.Set("ai", dummyAIConfig) // Patch kubernetes.NewClient to return a dummy client. patches := gomonkey.ApplyFunc(kubernetes.NewClient, func(kubecontext, kubeconfig string) (*kubernetes.Client, error) { return &kubernetes.Client{ Config: &rest.Config{Host: "fake-server"}, }, nil }) defer patches.Reset() // Patch ai.NewClient to return a NoOp client. patches2 := gomonkey.ApplyFunc(ai.NewClient, func(name string) ai.IAI { return &ai.NoOpAIClient{} }) defer patches2.Reset() output := util.CaptureOutput(func() { a, err := NewAnalysis( "", "english", []string{"Pod"}, "default", "", true, true, // explain 10, false, false, []string{}, false, ) require.NoError(t, err) a.Close() }) expectedOutputs := []string{ "Debug: Checking AI configuration.", "Debug: Using default AI provider dummy.", "Debug: AI configuration loaded, provider=dummy, baseUrl=http://dummy, model=dummy-model.", "Debug: Checking AI client initialization.", "Debug: AI client initialized.", } for _, expected := range expectedOutputs { if !util.Contains(output, expected) { t.Errorf("Expected output to contain: '%s', but got output: '%s'", expected, output) } } } // Test: Verbose output in RunAnalysis with filter flag func TestVerbose_RunAnalysisWithFilter(t *testing.T) { viper.Set("verbose", true) // Run analysis with a filter flag ("Pod") to trigger debug output. output := util.CaptureOutput(func() { _ = analysis_RunAnalysisFilterTester(t, "Pod") }) expectedOutputs := []string{ "Debug: Filter flags [Pod] specified, run selected core analyzers.", "Debug: PodAnalyzer launched.", "Debug: PodAnalyzer completed without errors.", } for _, expected := range expectedOutputs { if !util.Contains(output, expected) { t.Errorf("Expected output to contain: '%s', but got output: '%s'", expected, output) } } } // Test: Verbose output in RunAnalysis with active filter func TestVerbose_RunAnalysisWithActiveFilter(t *testing.T) { viper.Set("verbose", true) viper.SetDefault("active_filters", "Ingress") output := util.CaptureOutput(func() { _ = analysis_RunAnalysisFilterTester(t, "") }) expectedOutputs := []string{ "Debug: Found active filters [Ingress], run selected core analyzers.", "Debug: IngressAnalyzer launched.", "Debug: IngressAnalyzer completed without errors.", } for _, expected := range expectedOutputs { if !util.Contains(output, expected) { t.Errorf("Expected output to contain: '%s', but got output: '%s'", expected, output) } } } // Test: Verbose output in RunAnalysis without any filter (run all core analyzers) func TestVerbose_RunAnalysisWithoutFilter(t *testing.T) { viper.Set("verbose", true) // Clear filter flag and active_filters to run all core analyzers. viper.SetDefault("active_filters", []string{}) output := util.CaptureOutput(func() { _ = analysis_RunAnalysisFilterTester(t, "") }) // Check for debug message indicating no filters. expectedNoFilter := "Debug: No filters selected and no active filters found, run all core analyzers." if !util.Contains(output, expectedNoFilter) { t.Errorf("Expected output to contain: '%s', but got output: '%s'", expectedNoFilter, output) } // Get all core analyzers from analyzer.GetAnalyzerMap() coreAnalyzerMap, _ := analyzer.GetAnalyzerMap() for _, analyzerInstance := range coreAnalyzerMap { analyzerType := getTypeName(analyzerInstance) expectedLaunched := fmt.Sprintf("Debug: %s launched.", analyzerType) expectedCompleted := fmt.Sprintf("Debug: %s completed without errors.", analyzerType) if !util.Contains(output, expectedLaunched) { t.Errorf("Expected output to contain: '%s', but got output: '%s'", expectedLaunched, output) } if !util.Contains(output, expectedCompleted) { t.Errorf("Expected output to contain: '%s', but got output: '%s'", expectedCompleted, output) } } } // Test: Verbose output in RunCustomAnalysis without custom analyzer func TestVerbose_RunCustomAnalysisWithoutCustomAnalyzer(t *testing.T) { viper.Set("verbose", true) // Set custom_analyzers to empty array to trigger "No custom analyzers" debug message. viper.Set("custom_analyzers", []interface{}{}) analysisObj := &Analysis{ MaxConcurrency: 1, } output := util.CaptureOutput(func() { analysisObj.RunCustomAnalysis() }) expected := "Debug: No custom analyzers found." if !util.Contains(output, "Debug: No custom analyzers found.") { t.Errorf("Expected output to contain: '%s', but got output: '%s'", expected, output) } } // Test: Verbose output in RunCustomAnalysis with custom analyzer func TestVerbose_RunCustomAnalysisWithCustomAnalyzer(t *testing.T) { viper.Set("verbose", true) // Set custom_analyzers with one custom analyzer using "fake" connection. viper.Set("custom_analyzers", []map[string]interface{}{ { "name": "TestCustomAnalyzer", "connection": map[string]interface{}{"url": "127.0.0.1", "port": "2333"}, }, }) analysisObj := &Analysis{ MaxConcurrency: 1, } output := util.CaptureOutput(func() { analysisObj.RunCustomAnalysis() }) assert.Equal(t, 1, len(analysisObj.Errors)) // connection error expectedOutputs := []string{ "Debug: Found custom analyzers [TestCustomAnalyzer].", "Debug: TestCustomAnalyzer launched.", "Debug: TestCustomAnalyzer completed with errors.", } for _, expected := range expectedOutputs { if !util.Contains(output, expected) { t.Errorf("Expected output to contain: '%s', but got output: '%s'", expected, output) } } } // Test: Verbose output in GetAIResults func TestVerbose_GetAIResults(t *testing.T) { viper.Set("verbose", true) disabledCache := cache.New("disabled-cache") disabledCache.DisableCache() aiClient := &ai.NoOpAIClient{} analysisObj := Analysis{ AIClient: aiClient, Cache: disabledCache, Results: []common.Result{ { Kind: "Deployment", Name: "test-deployment", Error: []common.Failure{{Text: "test-problem", Sensitive: []common.Sensitive{}}}, Details: "test-solution", ParentObject: "parent-resource", }, }, Namespace: "default", } output := util.CaptureOutput(func() { _ = analysisObj.GetAIResults("json", false) }) expected := "Debug: Generating AI analysis." if !util.Contains(output, expected) { t.Errorf("Expected output to contain: '%s', but got output: '%s'", expected, output) } } ================================================ FILE: pkg/analysis/output.go ================================================ package analysis import ( "encoding/json" "fmt" "strings" "github.com/fatih/color" ) var outputFormats = map[string]func(*Analysis) ([]byte, error){ "json": (*Analysis).jsonOutput, "text": (*Analysis).textOutput, } func getOutputFormats() []string { formats := make([]string, 0, len(outputFormats)) for format := range outputFormats { formats = append(formats, format) } return formats } func (a *Analysis) PrintOutput(format string) ([]byte, error) { outputFunc, ok := outputFormats[format] if !ok { return nil, fmt.Errorf("unsupported output format: %s. Available format %s", format, strings.Join(getOutputFormats(), ",")) } return outputFunc(a) } func (a *Analysis) jsonOutput() ([]byte, error) { var problems int var status AnalysisStatus for _, result := range a.Results { problems += len(result.Error) } if problems > 0 { status = StateProblemDetected } else { status = StateOK } result := JsonOutput{ Provider: a.AnalysisAIProvider, Problems: problems, Results: a.Results, Errors: a.Errors, Status: status, } output, err := json.MarshalIndent(result, "", " ") if err != nil { return nil, fmt.Errorf("error marshalling json: %v", err) } return output, nil } func (a *Analysis) PrintStats() []byte { var output strings.Builder 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")) for _, stat := range a.Stats { output.WriteString(fmt.Sprintf("- Analyzer %s took %s \n", color.YellowString(stat.Analyzer), stat.DurationTime)) } return []byte(output.String()) } func (a *Analysis) textOutput() ([]byte, error) { var output strings.Builder // Print the AI provider used for this analysis (if explain was enabled). if a.Explain { output.WriteString(fmt.Sprintf("AI Provider: %s\n", color.YellowString(a.AnalysisAIProvider))) } else { output.WriteString(fmt.Sprintf("AI Provider: %s\n", color.YellowString("AI not used; --explain not set"))) } if len(a.Errors) != 0 { output.WriteString("\n") output.WriteString(color.YellowString("Warnings : \n")) for _, aerror := range a.Errors { output.WriteString(fmt.Sprintf("- %s\n", color.YellowString(aerror))) } } output.WriteString("\n") if len(a.Results) == 0 { output.WriteString(color.GreenString("No problems detected\n")) return []byte(output.String()), nil } for n, result := range a.Results { output.WriteString(fmt.Sprintf("%s: %s %s(%s)\n", color.CyanString("%d", n), color.HiYellowString(result.Kind), color.YellowString(result.Name), color.CyanString(result.ParentObject))) for _, err := range result.Error { output.WriteString(fmt.Sprintf("- %s %s\n", color.RedString("Error:"), color.RedString(err.Text))) if err.KubernetesDoc != "" { output.WriteString(fmt.Sprintf(" %s %s\n", color.RedString("Kubernetes Doc:"), color.RedString(err.KubernetesDoc))) } } output.WriteString(color.GreenString(result.Details + "\n")) } return []byte(output.String()), nil } ================================================ FILE: pkg/analysis/output_test.go ================================================ /* Copyright 2024 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package analysis import ( "testing" "github.com/stretchr/testify/require" ) func TestPrintOutput(t *testing.T) { require.NotEmpty(t, getOutputFormats()) tests := []struct { name string a *Analysis format string expectedOutput string expectedErr string }{ { name: "json format", a: &Analysis{}, format: "json", expectedOutput: "{\n \"provider\": \"\",\n \"errors\": null,\n \"status\": \"OK\",\n \"problems\": 0,\n \"results\": null\n}", }, { name: "text format", a: &Analysis{}, format: "text", expectedOutput: "AI Provider: AI not used; --explain not set\n\nNo problems detected\n", }, { name: "unsupported format", a: &Analysis{}, format: "unsupported", expectedErr: "unsupported output format: unsupported. Available format", }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { output, err := tt.a.PrintOutput(tt.format) if tt.expectedErr == "" { require.NoError(t, err) require.Contains(t, string(output), tt.expectedOutput) } else { require.ErrorContains(t, err, tt.expectedErr) require.Nil(t, output) } }) } } ================================================ FILE: pkg/analyzer/analyzer.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package analyzer import ( "fmt" "os" "github.com/fatih/color" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/integration" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" ) var ( AnalyzerErrorsMetric = promauto.NewGaugeVec(prometheus.GaugeOpts{ Name: "analyzer_errors", Help: "Number of errors detected by analyzer", }, []string{"analyzer_name", "object_name", "namespace"}) ) var coreAnalyzerMap = map[string]common.IAnalyzer{ "Pod": PodAnalyzer{}, "Deployment": DeploymentAnalyzer{}, "ReplicaSet": ReplicaSetAnalyzer{}, "PersistentVolumeClaim": PvcAnalyzer{}, "Service": ServiceAnalyzer{}, "Ingress": IngressAnalyzer{}, "StatefulSet": StatefulSetAnalyzer{}, "Job": JobAnalyzer{}, "CronJob": CronJobAnalyzer{}, "Node": NodeAnalyzer{}, "ValidatingWebhookConfiguration": ValidatingWebhookAnalyzer{}, "MutatingWebhookConfiguration": MutatingWebhookAnalyzer{}, "ConfigMap": ConfigMapAnalyzer{}, } var additionalAnalyzerMap = map[string]common.IAnalyzer{ "HorizontalPodAutoscaler": HpaAnalyzer{}, "PodDisruptionBudget": PdbAnalyzer{}, "NetworkPolicy": NetworkPolicyAnalyzer{}, "Log": LogAnalyzer{}, "GatewayClass": GatewayClassAnalyzer{}, "Gateway": GatewayAnalyzer{}, "HTTPRoute": HTTPRouteAnalyzer{}, "Storage": StorageAnalyzer{}, "Security": SecurityAnalyzer{}, "ClusterCatalog": ClusterCatalogAnalyzer{}, "ClusterExtension": ClusterExtensionAnalyzer{}, "ClusterServiceVersion": ClusterServiceVersionAnalyzer{}, "Subscription": SubscriptionAnalyzer{}, "InstallPlan": InstallPlanAnalyzer{}, "CatalogSource": CatalogSourceAnalyzer{}, "OperatorGroup": OperatorGroupAnalyzer{}, } func ListFilters() ([]string, []string, []string) { coreKeys := make([]string, 0, len(coreAnalyzerMap)) for k := range coreAnalyzerMap { coreKeys = append(coreKeys, k) } additionalKeys := make([]string, 0, len(additionalAnalyzerMap)) for k := range additionalAnalyzerMap { additionalKeys = append(additionalKeys, k) } integrationProvider := integration.NewIntegration() var integrationAnalyzers []string for _, i := range integrationProvider.List() { b, _ := integrationProvider.IsActivate(i) if b { in, err := integrationProvider.Get(i) if err != nil { fmt.Println(color.RedString(err.Error())) os.Exit(1) } integrationAnalyzers = append(integrationAnalyzers, in.GetAnalyzerName()...) } } return coreKeys, additionalKeys, integrationAnalyzers } func GetAnalyzerMap() (map[string]common.IAnalyzer, map[string]common.IAnalyzer) { coreAnalyzer := make(map[string]common.IAnalyzer) mergedAnalyzerMap := make(map[string]common.IAnalyzer) // add core analyzer for key, value := range coreAnalyzerMap { coreAnalyzer[key] = value mergedAnalyzerMap[key] = value } // add additional analyzer for key, value := range additionalAnalyzerMap { mergedAnalyzerMap[key] = value } integrationProvider := integration.NewIntegration() for _, i := range integrationProvider.List() { b, err := integrationProvider.IsActivate(i) if err != nil { fmt.Println(color.RedString(err.Error())) os.Exit(1) } if b { in, err := integrationProvider.Get(i) if err != nil { fmt.Println(color.RedString(err.Error())) os.Exit(1) } in.AddAnalyzer(&mergedAnalyzerMap) } } return coreAnalyzer, mergedAnalyzerMap } ================================================ FILE: pkg/analyzer/catalogsource.go ================================================ package analyzer import ( "fmt" "strings" "github.com/k8sgpt-ai/k8sgpt/pkg/common" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" ) type CatalogSourceAnalyzer struct{} var catSrcGVR = schema.GroupVersionResource{ Group: "operators.coreos.com", Version: "v1alpha1", Resource: "catalogsources", } func (CatalogSourceAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) { kind := "CatalogSource" if a.Client.GetDynamicClient() == nil { return nil, fmt.Errorf("dynamic client is nil in %s analyzer", kind) } list, err := a.Client.GetDynamicClient(). Resource(catSrcGVR).Namespace(metav1.NamespaceAll). List(a.Context, metav1.ListOptions{}) if err != nil { return nil, err } var results []common.Result for _, item := range list.Items { ns, name := item.GetNamespace(), item.GetName() state, _, _ := unstructured.NestedString(item.Object, "status", "connectionState", "lastObservedState") addr, _, _ := unstructured.NestedString(item.Object, "status", "connectionState", "address") // Only report if state is present and not READY if state != "" && strings.ToUpper(state) != "READY" { results = append(results, common.Result{ Kind: kind, Name: ns + "/" + name, Error: []common.Failure{{ Text: fmt.Sprintf("connectionState=%s (address=%s)", state, addr), }}, }) } } return results, nil } ================================================ FILE: pkg/analyzer/catalogsource_test.go ================================================ package analyzer import ( "context" "strings" "testing" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" dynamicfake "k8s.io/client-go/dynamic/fake" ) func TestCatalogSourceAnalyzer_UnhealthyState_ReturnsResult(t *testing.T) { cs := &unstructured.Unstructured{ Object: map[string]any{ "apiVersion": "operators.coreos.com/v1alpha1", "kind": "CatalogSource", "metadata": map[string]any{ "name": "broken-operators-external", "namespace": "openshift-marketplace", }, "status": map[string]any{ "connectionState": map[string]any{ "lastObservedState": "TRANSIENT_FAILURE", "address": "not-a-real-host.invalid:50051", }, }, }, } listKinds := map[schema.GroupVersionResource]string{ {Group: "operators.coreos.com", Version: "v1alpha1", Resource: "catalogsources"}: "CatalogSourceList", } scheme := runtime.NewScheme() dc := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(scheme, listKinds, cs) a := common.Analyzer{ Context: context.TODO(), Client: &kubernetes.Client{DynamicClient: dc}, } res, err := (CatalogSourceAnalyzer{}).Analyze(a) if err != nil { t.Fatalf("Analyze error: %v", err) } if len(res) != 1 { t.Fatalf("expected 1 result, got %d", len(res)) } if res[0].Kind != "CatalogSource" || !strings.Contains(res[0].Name, "openshift-marketplace/broken-operators-external") { t.Fatalf("unexpected result: %#v", res[0]) } if len(res[0].Error) == 0 || !strings.Contains(res[0].Error[0].Text, "TRANSIENT_FAILURE") { t.Fatalf("expected TRANSIENT_FAILURE in message, got %#v", res[0].Error) } } func TestCatalogSourceAnalyzer_HealthyOrNoState_Ignored(t *testing.T) { // One READY (healthy), one with no status at all: both should be ignored. ready := &unstructured.Unstructured{ Object: map[string]any{ "apiVersion": "operators.coreos.com/v1alpha1", "kind": "CatalogSource", "metadata": map[string]any{ "name": "ready-operators", "namespace": "openshift-marketplace", }, "status": map[string]any{ "connectionState": map[string]any{ "lastObservedState": "READY", "address": "somewhere", }, }, }, } nostate := &unstructured.Unstructured{ Object: map[string]any{ "apiVersion": "operators.coreos.com/v1alpha1", "kind": "CatalogSource", "metadata": map[string]any{ "name": "no-status-operators", "namespace": "openshift-marketplace", }, }, } listKinds := map[schema.GroupVersionResource]string{ {Group: "operators.coreos.com", Version: "v1alpha1", Resource: "catalogsources"}: "CatalogSourceList", } scheme := runtime.NewScheme() dc := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(scheme, listKinds, ready, nostate) a := common.Analyzer{ Context: context.TODO(), Client: &kubernetes.Client{DynamicClient: dc}, } res, err := (CatalogSourceAnalyzer{}).Analyze(a) if err != nil { t.Fatalf("Analyze error: %v", err) } if len(res) != 0 { t.Fatalf("expected 0 results (healthy/nostate ignored), got %d", len(res)) } } ================================================ FILE: pkg/analyzer/clustercatalog.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package analyzer import ( "fmt" "regexp" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/util" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" ) type ClusterCatalogAnalyzer struct{} func (ClusterCatalogAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) { kind := "ClusterCatalog" AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{ "analyzer_name": kind, }) var clusterCatalogGVR = schema.GroupVersionResource{ Group: "olm.operatorframework.io", Version: "v1", Resource: "clustercatalogs", } if a.Client == nil { return nil, fmt.Errorf("client is nil in ClusterCatalogAnalyzer") } if a.Client.GetDynamicClient() == nil { return nil, fmt.Errorf("dynamic client is nil in ClusterCatalogAnalyzer") } list, err := a.Client.GetDynamicClient().Resource(clusterCatalogGVR).Namespace("").List(a.Context, metav1.ListOptions{}) if err != nil { return nil, err } var preAnalysis = map[string]common.PreAnalysis{} for _, item := range list.Items { var failures []common.Failure catalog, err := ConvertToClusterCatalog(&item) if err != nil { continue } fmt.Printf("ClusterCatalog: %s | Source: %s\n", catalog.Name, catalog.Spec.Source.Image.Ref) failures, err = ValidateClusterCatalog(failures, catalog) if err != nil { continue } if len(failures) > 0 { preAnalysis[catalog.Name] = common.PreAnalysis{ Catalog: *catalog, FailureDetails: failures, } AnalyzerErrorsMetric.WithLabelValues(kind, catalog.Name, "").Set(float64(len(failures))) } } for key, value := range preAnalysis { var currentAnalysis = common.Result{ Kind: kind, Name: key, Error: value.FailureDetails, } parent, found := util.GetParent(a.Client, value.Node.ObjectMeta) if found { currentAnalysis.ParentObject = parent } a.Results = append(a.Results, currentAnalysis) } return a.Results, err } func ConvertToClusterCatalog(u *unstructured.Unstructured) (*common.ClusterCatalog, error) { var cc common.ClusterCatalog err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &cc) if err != nil { return nil, fmt.Errorf("failed to convert to ClusterCatalog: %w", err) } return &cc, nil } func addCatalogConditionFailure(failures []common.Failure, catalogName string, catalogCondition metav1.Condition) []common.Failure { failures = append(failures, common.Failure{ Text: fmt.Sprintf("OLMv1 ClusterCatalog: %s has condition of type %s, reason %s: %s", catalogName, catalogCondition.Type, catalogCondition.Reason, catalogCondition.Message), Sensitive: []common.Sensitive{ { Unmasked: catalogName, Masked: util.MaskString(catalogName), }, }, }) return failures } func addCatalogFailure(failures []common.Failure, catalogName string, err error) []common.Failure { failures = append(failures, common.Failure{ Text: fmt.Sprintf("%s has error: %s", catalogName, err.Error()), Sensitive: []common.Sensitive{ { Unmasked: catalogName, Masked: util.MaskString(catalogName), }, }, }) return failures } func ValidateClusterCatalog(failures []common.Failure, catalog *common.ClusterCatalog) ([]common.Failure, error) { if !isValidImageRef(catalog.Spec.Source.Image.Ref) { failures = addCatalogFailure(failures, catalog.Name, fmt.Errorf("invalid image ref format in spec.source.image.ref: %s", catalog.Spec.Source.Image.Ref)) } // Check status.resolvedSource.image.ref ends with @sha256:... if catalog.Status.ResolvedSource != nil { if catalog.Status.ResolvedSource.Image.Ref == "" { failures = addCatalogFailure(failures, catalog.Name, fmt.Errorf("missing status.resolvedSource.image.ref")) } if !regexp.MustCompile(`@sha256:[a-f0-9]{64}$`).MatchString(catalog.Status.ResolvedSource.Image.Ref) { failures = addCatalogFailure(failures, catalog.Name, fmt.Errorf("status.resolvedSource.image.ref must end with @sha256:")) } } for _, condition := range catalog.Status.Conditions { if condition.Status != "True" && condition.Type == "Serving" { failures = addCatalogConditionFailure(failures, catalog.Name, condition) } if condition.Type == "Progressing" && condition.Reason != "Succeeded" { failures = addCatalogConditionFailure(failures, catalog.Name, condition) } } return failures, nil } // isValidImageRef does a simple regex check to validate image refs func isValidImageRef(ref string) bool { pattern := `^([a-zA-Z0-9\-\.]+(?::[0-9]+)?/)?([a-z0-9]+(?:[._\-\/][a-z0-9]+)*)(:[\w][\w.-]{0,127})?(?:@sha256:[a-f0-9]{64})?$` return regexp.MustCompile(pattern).MatchString(ref) } ================================================ FILE: pkg/analyzer/clustercatalog_test.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package analyzer import ( "context" "fmt" "testing" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" dynamicfake "k8s.io/client-go/dynamic/fake" "k8s.io/client-go/kubernetes/fake" ) func TestClusterCatalogAnalyzer(t *testing.T) { gvr := schema.GroupVersionResource{ Group: "olm.operatorframework.io", Version: "v1", Resource: "clustercatalogs", } scheme := runtime.NewScheme() dynamicClient := dynamicfake.NewSimpleDynamicClientWithCustomListKinds( scheme, map[schema.GroupVersionResource]string{ gvr: "ClusterCatalogList", }, &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": "olm.operatorframework.io/v1", "kind": "ClusterCatalog", "metadata": map[string]interface{}{ "name": "Valid ClusterCatalog", }, "spec": map[string]interface{}{ "availabilityMode": "Available", "source": map[string]interface{}{ "type": "Image", "image": map[string]interface{}{ "ref": "registry.redhat.io/redhat/community-operator-index:v4.19", "pollIntervalMinutes": float64(10), }, }, }, "status": map[string]interface{}{ "conditions": []interface{}{ map[string]interface{}{ "type": "Progressing", "status": "True", "reason": "Succeeded", }, map[string]interface{}{ "type": "Serving", "status": "True", "reason": "Available", }, }, }, }, }, &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": "olm.operatorframework.io/v1", "kind": "ClusterCatalog", "metadata": map[string]interface{}{ "name": "Invalid availabilityMode", }, "spec": map[string]interface{}{ "availabilityMode": "test", "source": map[string]interface{}{ "type": "Image", "image": map[string]interface{}{ "ref": "registry.redhat.io/redhat/community-operator-index:v4.19", "pollIntervalMinutes": float64(10), }, }, }, "status": map[string]interface{}{ "conditions": []interface{}{ map[string]interface{}{ "type": "Progressing", "status": "True", "reason": "Retrying", }, }, }, }, }, &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": "olm.operatorframework.io/v1", "kind": "ClusterCatalog", "metadata": map[string]interface{}{ "name": "Invalid pollIntervalMinutes", }, "spec": map[string]interface{}{ "availabilityMode": "Available", "source": map[string]interface{}{ "type": "Image", "image": map[string]interface{}{ "ref": "registry.redhat.io/redhat/community-operator-index:v4.19", "pollIntervalMinutes": float64(0), }, }, }, "status": map[string]interface{}{ "conditions": []interface{}{ map[string]interface{}{ "type": "Progressing", "status": "True", "reason": "Retrying", }, }, }, }, }, &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": "olm.operatorframework.io/v1", "kind": "ClusterCatalog", "metadata": map[string]interface{}{ "name": "Invalid image reference", }, "spec": map[string]interface{}{ "availabilityMode": "Available", "source": map[string]interface{}{ "type": "Image", "image": map[string]interface{}{ "ref": "quay.io/test/community-operator-index:v4.19", "pollIntervalMinutes": float64(10), }, }, }, "status": map[string]interface{}{ "conditions": []interface{}{ map[string]interface{}{ "type": "Progressing", "status": "True", "reason": "Retrying", }, }, }, }, }, ) config := common.Analyzer{ Client: &kubernetes.Client{ Client: fake.NewSimpleClientset(), DynamicClient: dynamicClient, }, Context: context.Background(), Namespace: "test", } ccAnalyzer := ClusterCatalogAnalyzer{} results, err := ccAnalyzer.Analyze(config) for _, res := range results { fmt.Printf("Result: %s | Failures: %d\n", res.Name, len(res.Error)) for _, err := range res.Error { fmt.Printf(" - %s\n", err) } } require.NoError(t, err) require.Equal(t, 3, len(results)) } ================================================ FILE: pkg/analyzer/clusterextension.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package analyzer import ( "fmt" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/util" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" ) type ClusterExtensionAnalyzer struct{} func (ClusterExtensionAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) { kind := "ClusterExtension" AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{ "analyzer_name": kind, }) var clusterExtensionGVR = schema.GroupVersionResource{ Group: "olm.operatorframework.io", Version: "v1", Resource: "clusterextensions", } if a.Client == nil { return nil, fmt.Errorf("client is nil in ClusterExtensionAnalyzer") } if a.Client.GetDynamicClient() == nil { return nil, fmt.Errorf("dynamic client is nil in ClusterExtensionAnalyzer") } list, err := a.Client.GetDynamicClient().Resource(clusterExtensionGVR).Namespace("").List(a.Context, metav1.ListOptions{}) if err != nil { return nil, err } var preAnalysis = map[string]common.PreAnalysis{} for _, item := range list.Items { var failures []common.Failure extension, err := ConvertToClusterExtension(&item) if err != nil { continue } fmt.Printf("ClusterExtension: %s | Source: %s\n", extension.Name, extension.Spec.Source.Catalog.PackageName) failures, err = ValidateClusterExtension(failures, extension) if err != nil { continue } if len(failures) > 0 { preAnalysis[extension.Name] = common.PreAnalysis{ Extension: *extension, FailureDetails: failures, } AnalyzerErrorsMetric.WithLabelValues(kind, extension.Name, "").Set(float64(len(failures))) } } for key, value := range preAnalysis { var currentAnalysis = common.Result{ Kind: kind, Name: key, Error: value.FailureDetails, } parent, found := util.GetParent(a.Client, value.Node.ObjectMeta) if found { currentAnalysis.ParentObject = parent } a.Results = append(a.Results, currentAnalysis) } return a.Results, err } func ConvertToClusterExtension(u *unstructured.Unstructured) (*common.ClusterExtension, error) { var ce common.ClusterExtension err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &ce) if err != nil { return nil, fmt.Errorf("failed to convert to ClusterExtension: %w", err) } return &ce, nil } func addExtensionConditionFailure(failures []common.Failure, extensionName string, extensionCondition metav1.Condition) []common.Failure { failures = append(failures, common.Failure{ Text: fmt.Sprintf("OLMv1 ClusterExtension: %s has condition of type %s, reason %s: %s", extensionName, extensionCondition.Type, extensionCondition.Reason, extensionCondition.Message), Sensitive: []common.Sensitive{ { Unmasked: extensionName, Masked: util.MaskString(extensionName), }, }, }) return failures } func addExtensionFailure(failures []common.Failure, extensionName string, err error) []common.Failure { failures = append(failures, common.Failure{ Text: fmt.Sprintf("%s has error: %s", extensionName, err.Error()), Sensitive: []common.Sensitive{ { Unmasked: extensionName, Masked: util.MaskString(extensionName), }, }, }) return failures } func ValidateClusterExtension(failures []common.Failure, extension *common.ClusterExtension) ([]common.Failure, error) { if extension.Spec.Source.Catalog != nil && extension.Spec.Source.Catalog.UpgradeConstraintPolicy != "CatalogProvided" && extension.Spec.Source.Catalog.UpgradeConstraintPolicy != "SelfCertified" { failures = addExtensionFailure(failures, extension.Name, fmt.Errorf("invalid or missing extension.Spec.Source.Catalog.UpgradeConstraintPolicy (expecting 'SelfCertified' or 'CatalogProvided')")) } if extension.Spec.Source.SourceType != "Catalog" { failures = addExtensionFailure(failures, extension.Name, fmt.Errorf("invalid or missing spec.source.sourceType (expecting 'Catalog')")) } for _, condition := range extension.Status.Conditions { if condition.Status != "True" && condition.Type == "Installed" { failures = addExtensionConditionFailure(failures, extension.Name, condition) } if condition.Type == "Progressing" && condition.Reason != "Succeeded" { failures = addExtensionConditionFailure(failures, extension.Name, condition) } } return failures, nil } ================================================ FILE: pkg/analyzer/clusterextension_test.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package analyzer import ( "context" "fmt" "testing" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" dynamicfake "k8s.io/client-go/dynamic/fake" "k8s.io/client-go/kubernetes/fake" ) func TestClusterExtensionAnalyzer(t *testing.T) { gvr := schema.GroupVersionResource{ Group: "olm.operatorframework.io", Version: "v1", Resource: "clusterextensions", } scheme := runtime.NewScheme() dynamicClient := dynamicfake.NewSimpleDynamicClientWithCustomListKinds( scheme, map[schema.GroupVersionResource]string{ gvr: "ClusterExtensionList", }, &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": "olm.operatorframework.io/v1", "kind": "ClusterExtension", "metadata": map[string]interface{}{ "name": "Valid SelfCertified ClusterExtension", }, "spec": map[string]interface{}{ "source": map[string]interface{}{ "sourceType": "Catalog", "catalog": map[string]interface{}{ "upgradeConstraintPolicy": "SelfCertified", }, }, }, "status": map[string]interface{}{ "conditions": []interface{}{ map[string]interface{}{ "type": "Installed", "status": "True", "reason": "Succeeded", }, }, }, }, }, &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": "olm.operatorframework.io/v1", "kind": "ClusterExtension", "metadata": map[string]interface{}{ "name": "Valid CatalogProvided ClusterExtension", }, "spec": map[string]interface{}{ "source": map[string]interface{}{ "sourceType": "Catalog", "catalog": map[string]interface{}{ "upgradeConstraintPolicy": "CatalogProvided", }, }, }, "status": map[string]interface{}{ "conditions": []interface{}{ map[string]interface{}{ "type": "Installed", "status": "True", "reason": "Succeeded", }, }, }, }, }, &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": "olm.operatorframework.io/v1", "kind": "ClusterExtension", "metadata": map[string]interface{}{ "name": "Invalid UpgradeConstraintPolicy", }, "spec": map[string]interface{}{ "source": map[string]interface{}{ "sourceType": "Catalog", "catalog": map[string]interface{}{ "upgradeConstraintPolicy": "InvalidPolicy", }, }, }, "status": map[string]interface{}{ "conditions": []interface{}{ map[string]interface{}{ "type": "Progressing", "status": "True", "reason": "Retrying", }, map[string]interface{}{ "type": "Installed", "status": "False", "reason": "Failed", }, }, }, }, }, &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": "olm.operatorframework.io/v1", "kind": "ClusterExtension", "metadata": map[string]interface{}{ "name": "Invalid SourceType", }, "spec": map[string]interface{}{ "source": map[string]interface{}{ "sourceType": "Git", "catalog": map[string]interface{}{ "upgradeConstraintPolicy": "CatalogProvided", }, }, }, "status": map[string]interface{}{ "conditions": []interface{}{ map[string]interface{}{ "type": "Progressing", "status": "True", "reason": "Retrying", }, map[string]interface{}{ "type": "Installed", "status": "False", "reason": "Failed", }, }, }, }, }, ) config := common.Analyzer{ Client: &kubernetes.Client{ Client: fake.NewSimpleClientset(), DynamicClient: dynamicClient, }, Context: context.Background(), Namespace: "test", } ceAnalyzer := ClusterExtensionAnalyzer{} results, err := ceAnalyzer.Analyze(config) for _, res := range results { fmt.Printf("Result: %s | Failures: %d\n", res.Name, len(res.Error)) for _, err := range res.Error { fmt.Printf(" - %s\n", err) } } require.NoError(t, err) require.Equal(t, 2, len(results)) } ================================================ FILE: pkg/analyzer/clusterserviceversion.go ================================================ package analyzer import ( "fmt" "github.com/k8sgpt-ai/k8sgpt/pkg/common" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" ) type ClusterServiceVersionAnalyzer struct{} var csvGVR = schema.GroupVersionResource{ Group: "operators.coreos.com", Version: "v1alpha1", Resource: "clusterserviceversions", } func (ClusterServiceVersionAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) { kind := "ClusterServiceVersion" if a.Client.GetDynamicClient() == nil { return nil, fmt.Errorf("dynamic client is nil in %s analyzer", kind) } list, err := a.Client.GetDynamicClient(). Resource(csvGVR).Namespace(metav1.NamespaceAll). List(a.Context, metav1.ListOptions{}) if err != nil { return nil, err } var results []common.Result for _, item := range list.Items { ns := item.GetNamespace() name := item.GetName() phase, _, _ := unstructured.NestedString(item.Object, "status", "phase") var failures []common.Failure if phase != "" && phase != "Succeeded" { // Superfície de condições para contexto if conds, _, _ := unstructured.NestedSlice(item.Object, "status", "conditions"); len(conds) > 0 { if msg := pickWorstCondition(conds); msg != "" { failures = append(failures, common.Failure{Text: fmt.Sprintf("phase=%q: %s", phase, msg)}) } } else { failures = append(failures, common.Failure{Text: fmt.Sprintf("phase=%q (see status.conditions)", phase)}) } } if len(failures) > 0 { results = append(results, common.Result{ Kind: kind, Name: ns + "/" + name, Error: failures, }) } } return results, nil } // reaproveitamos o heurístico já usado em outros pontos func pickWorstCondition(conds []interface{}) string { for _, c := range conds { m, ok := c.(map[string]any) if !ok { continue } if s, _ := m["status"].(string); s == "True" { continue } r, _ := m["reason"].(string) msg, _ := m["message"].(string) if r == "" && msg == "" { continue } if r != "" && msg != "" { return r + ": " + msg } return r + msg } return "" } ================================================ FILE: pkg/analyzer/clusterserviceversion_test.go ================================================ package analyzer import ( "context" "strings" "testing" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" dynamicfake "k8s.io/client-go/dynamic/fake" ) func TestClusterServiceVersionAnalyzer(t *testing.T) { ok := &unstructured.Unstructured{ Object: map[string]any{ "apiVersion": "operators.coreos.com/v1alpha1", "kind": "ClusterServiceVersion", "metadata": map[string]any{ "name": "ok", "namespace": "ns1", }, "status": map[string]any{"phase": "Succeeded"}, }, } bad := &unstructured.Unstructured{ Object: map[string]any{ "apiVersion": "operators.coreos.com/v1alpha1", "kind": "ClusterServiceVersion", "metadata": map[string]any{ "name": "bad", "namespace": "ns1", }, "status": map[string]any{ "phase": "Failed", // IMPORTANT: conditions must be []interface{}, not []map[string]any "conditions": []interface{}{ map[string]any{ "status": "False", "reason": "ErrorResolving", "message": "missing dep", }, }, }, }, } listKinds := map[schema.GroupVersionResource]string{ {Group: "operators.coreos.com", Version: "v1alpha1", Resource: "clusterserviceversions"}: "ClusterServiceVersionList", } // Use a non-nil scheme with dynamicfake scheme := runtime.NewScheme() dc := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(scheme, listKinds, ok, bad) a := common.Analyzer{ Context: context.TODO(), Client: &kubernetes.Client{DynamicClient: dc}, } res, err := (ClusterServiceVersionAnalyzer{}).Analyze(a) if err != nil { t.Fatalf("Analyze error: %v", err) } if len(res) != 1 { t.Fatalf("expected 1 result, got %d", len(res)) } if res[0].Kind != "ClusterServiceVersion" || !strings.Contains(res[0].Name, "ns1/bad") { t.Fatalf("unexpected result: %#v", res[0]) } if len(res[0].Error) == 0 || !strings.Contains(res[0].Error[0].Text, "missing dep") { t.Fatalf("expected 'missing dep' in failure, got %#v", res[0].Error) } } ================================================ FILE: pkg/analyzer/configmap.go ================================================ /* Copyright 2024 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package analyzer import ( "fmt" "github.com/k8sgpt-ai/k8sgpt/pkg/common" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) type ConfigMapAnalyzer struct{} func (ConfigMapAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) { kind := "ConfigMap" AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{ "analyzer_name": kind, }) // Get all ConfigMaps in the namespace configMaps, err := a.Client.GetClient().CoreV1().ConfigMaps(a.Namespace).List(a.Context, metav1.ListOptions{ LabelSelector: a.LabelSelector, }) if err != nil { return nil, err } // Get all Pods to check ConfigMap usage pods, err := a.Client.GetClient().CoreV1().Pods(a.Namespace).List(a.Context, metav1.ListOptions{}) if err != nil { return nil, err } var results []common.Result // Track which ConfigMaps are used usedConfigMaps := make(map[string]bool) configMapUsage := make(map[string][]string) // maps ConfigMap name to list of pods using it // Analyze ConfigMap usage in Pods for _, pod := range pods.Items { // Check volume mounts for _, volume := range pod.Spec.Volumes { if volume.ConfigMap != nil { usedConfigMaps[volume.ConfigMap.Name] = true configMapUsage[volume.ConfigMap.Name] = append(configMapUsage[volume.ConfigMap.Name], pod.Name) } } // Check environment variables for _, container := range pod.Spec.Containers { for _, env := range container.EnvFrom { if env.ConfigMapRef != nil { usedConfigMaps[env.ConfigMapRef.Name] = true configMapUsage[env.ConfigMapRef.Name] = append(configMapUsage[env.ConfigMapRef.Name], pod.Name) } } for _, env := range container.Env { if env.ValueFrom != nil && env.ValueFrom.ConfigMapKeyRef != nil { usedConfigMaps[env.ValueFrom.ConfigMapKeyRef.Name] = true configMapUsage[env.ValueFrom.ConfigMapKeyRef.Name] = append(configMapUsage[env.ValueFrom.ConfigMapKeyRef.Name], pod.Name) } } } } // Analyze each ConfigMap for _, cm := range configMaps.Items { var failures []common.Failure // Check for unused ConfigMaps if !usedConfigMaps[cm.Name] { failures = append(failures, common.Failure{ Text: fmt.Sprintf("ConfigMap %s is not used by any pods in the namespace", cm.Name), Sensitive: []common.Sensitive{}, }) } // Check for empty ConfigMaps if len(cm.Data) == 0 && len(cm.BinaryData) == 0 { failures = append(failures, common.Failure{ Text: fmt.Sprintf("ConfigMap %s is empty", cm.Name), Sensitive: []common.Sensitive{}, }) } // Check for large ConfigMaps (over 1MB) totalSize := 0 for _, value := range cm.Data { totalSize += len(value) } for _, value := range cm.BinaryData { totalSize += len(value) } if totalSize > 1024*1024 { // 1MB failures = append(failures, common.Failure{ Text: fmt.Sprintf("ConfigMap %s is larger than 1MB (%d bytes)", cm.Name, totalSize), Sensitive: []common.Sensitive{}, }) } if len(failures) > 0 { results = append(results, common.Result{ Kind: kind, Name: fmt.Sprintf("%s/%s", cm.Namespace, cm.Name), Error: failures, }) AnalyzerErrorsMetric.WithLabelValues(kind, cm.Name, cm.Namespace).Set(float64(len(failures))) } } return results, nil } ================================================ FILE: pkg/analyzer/configmap_test.go ================================================ /* Copyright 2024 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package analyzer import ( "context" "testing" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" "github.com/stretchr/testify/assert" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/fake" ) func TestConfigMapAnalyzer(t *testing.T) { tests := []struct { name string namespace string configMaps []v1.ConfigMap pods []v1.Pod expectedErrors int }{ { name: "unused configmap", namespace: "default", configMaps: []v1.ConfigMap{ { ObjectMeta: metav1.ObjectMeta{ Name: "unused-cm", Namespace: "default", }, Data: map[string]string{ "key": "value", }, }, }, expectedErrors: 1, }, { name: "empty configmap", namespace: "default", configMaps: []v1.ConfigMap{ { ObjectMeta: metav1.ObjectMeta{ Name: "empty-cm", Namespace: "default", }, }, }, expectedErrors: 1, }, { name: "large configmap", namespace: "default", configMaps: []v1.ConfigMap{ { ObjectMeta: metav1.ObjectMeta{ Name: "large-cm", Namespace: "default", }, Data: map[string]string{ "key": string(make([]byte, 1024*1024+1)), // 1MB + 1 byte }, }, }, expectedErrors: 1, }, { name: "used configmap", namespace: "default", configMaps: []v1.ConfigMap{ { ObjectMeta: metav1.ObjectMeta{ Name: "used-cm", Namespace: "default", }, Data: map[string]string{ "key": "value", }, }, }, pods: []v1.Pod{ { ObjectMeta: metav1.ObjectMeta{ Name: "test-pod", Namespace: "default", }, Spec: v1.PodSpec{ Containers: []v1.Container{ { Name: "test-container", EnvFrom: []v1.EnvFromSource{ { ConfigMapRef: &v1.ConfigMapEnvSource{ LocalObjectReference: v1.LocalObjectReference{ Name: "used-cm", }, }, }, }, }, }, }, }, }, expectedErrors: 0, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { client := fake.NewSimpleClientset() // Create test resources for _, cm := range tt.configMaps { _, err := client.CoreV1().ConfigMaps(tt.namespace).Create(context.TODO(), &cm, metav1.CreateOptions{}) assert.NoError(t, err) } for _, pod := range tt.pods { _, err := client.CoreV1().Pods(tt.namespace).Create(context.TODO(), &pod, metav1.CreateOptions{}) assert.NoError(t, err) } analyzer := ConfigMapAnalyzer{} results, err := analyzer.Analyze(common.Analyzer{ Client: &kubernetes.Client{Client: client}, Context: context.TODO(), Namespace: tt.namespace, }) assert.NoError(t, err) assert.Equal(t, tt.expectedErrors, len(results)) }) } } ================================================ FILE: pkg/analyzer/cronjob.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package analyzer import ( "fmt" "time" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" "github.com/k8sgpt-ai/k8sgpt/pkg/util" cron "github.com/robfig/cron/v3" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" ) type CronJobAnalyzer struct{} func (analyzer CronJobAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) { kind := "CronJob" apiDoc := kubernetes.K8sApiReference{ Kind: kind, ApiVersion: schema.GroupVersion{ Group: "batch", Version: "v1", }, OpenapiSchema: a.OpenapiSchema, } AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{ "analyzer_name": kind, }) cronJobList, err := a.Client.GetClient().BatchV1().CronJobs(a.Namespace).List(a.Context, v1.ListOptions{LabelSelector: a.LabelSelector}) if err != nil { return nil, err } var preAnalysis = map[string]common.PreAnalysis{} for _, cronJob := range cronJobList.Items { var failures []common.Failure if cronJob.Spec.Suspend != nil && *cronJob.Spec.Suspend { doc := apiDoc.GetApiDocV2("spec.suspend") failures = append(failures, common.Failure{ Text: fmt.Sprintf("CronJob %s is suspended", cronJob.Name), KubernetesDoc: doc, Sensitive: []common.Sensitive{ { Unmasked: cronJob.Namespace, Masked: util.MaskString(cronJob.Namespace), }, { Unmasked: cronJob.Name, Masked: util.MaskString(cronJob.Name), }, }, }) } else { // check the schedule format if _, err := CheckCronScheduleIsValid(cronJob.Spec.Schedule); err != nil { doc := apiDoc.GetApiDocV2("spec.schedule") failures = append(failures, common.Failure{ Text: fmt.Sprintf("CronJob %s has an invalid schedule: %s", cronJob.Name, err.Error()), KubernetesDoc: doc, Sensitive: []common.Sensitive{ { Unmasked: cronJob.Namespace, Masked: util.MaskString(cronJob.Namespace), }, { Unmasked: cronJob.Name, Masked: util.MaskString(cronJob.Name), }, }, }) } // check the starting deadline if cronJob.Spec.StartingDeadlineSeconds != nil { deadline := time.Duration(*cronJob.Spec.StartingDeadlineSeconds) * time.Second if deadline < 0 { doc := apiDoc.GetApiDocV2("spec.startingDeadlineSeconds") failures = append(failures, common.Failure{ Text: fmt.Sprintf("CronJob %s has a negative starting deadline", cronJob.Name), KubernetesDoc: doc, Sensitive: []common.Sensitive{ { Unmasked: cronJob.Namespace, Masked: util.MaskString(cronJob.Namespace), }, { Unmasked: cronJob.Name, Masked: util.MaskString(cronJob.Name), }, }, }) } } } if len(failures) > 0 { preAnalysis[fmt.Sprintf("%s/%s", cronJob.Namespace, cronJob.Name)] = common.PreAnalysis{ FailureDetails: failures, } AnalyzerErrorsMetric.WithLabelValues(kind, cronJob.Name, cronJob.Namespace).Set(float64(len(failures))) } } for key, value := range preAnalysis { currentAnalysis := common.Result{ Kind: kind, Name: key, Error: value.FailureDetails, } a.Results = append(a.Results, currentAnalysis) } return a.Results, nil } // Check CRON schedule format func CheckCronScheduleIsValid(schedule string) (bool, error) { _, err := cron.ParseStandard(schedule) if err != nil { return false, err } return true, nil } ================================================ FILE: pkg/analyzer/cronjob_test.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package analyzer import ( "context" "sort" "testing" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" "github.com/stretchr/testify/require" batchv1 "k8s.io/api/batch/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/fake" ) func TestCronJobAnalyzer(t *testing.T) { tests := []struct { name string config common.Analyzer expectations []struct { name string failuresCount int } }{ { name: "Suspended CronJob", config: common.Analyzer{ Client: &kubernetes.Client{ Client: fake.NewSimpleClientset( &batchv1.CronJob{ ObjectMeta: metav1.ObjectMeta{ Name: "suspended-job", Namespace: "default", }, Spec: batchv1.CronJobSpec{ Schedule: "*/5 * * * *", Suspend: boolPtr(true), }, }, ), }, Context: context.Background(), Namespace: "default", }, expectations: []struct { name string failuresCount int }{ { name: "default/suspended-job", failuresCount: 1, // One failure for being suspended }, }, }, { name: "Invalid schedule format", config: common.Analyzer{ Client: &kubernetes.Client{ Client: fake.NewSimpleClientset( &batchv1.CronJob{ ObjectMeta: metav1.ObjectMeta{ Name: "invalid-schedule", Namespace: "default", }, Spec: batchv1.CronJobSpec{ Schedule: "invalid-cron", // Invalid cron format }, }, ), }, Context: context.Background(), Namespace: "default", }, expectations: []struct { name string failuresCount int }{ { name: "default/invalid-schedule", failuresCount: 1, // One failure for invalid schedule }, }, }, { name: "Negative starting deadline", config: common.Analyzer{ Client: &kubernetes.Client{ Client: fake.NewSimpleClientset( &batchv1.CronJob{ ObjectMeta: metav1.ObjectMeta{ Name: "negative-deadline", Namespace: "default", }, Spec: batchv1.CronJobSpec{ Schedule: "*/5 * * * *", StartingDeadlineSeconds: int64Ptr(-60), // Negative deadline }, }, ), }, Context: context.Background(), Namespace: "default", }, expectations: []struct { name string failuresCount int }{ { name: "default/negative-deadline", failuresCount: 1, // One failure for negative deadline }, }, }, { name: "Valid CronJob", config: common.Analyzer{ Client: &kubernetes.Client{ Client: fake.NewSimpleClientset( &batchv1.CronJob{ ObjectMeta: metav1.ObjectMeta{ Name: "valid-job", Namespace: "default", }, Spec: batchv1.CronJobSpec{ Schedule: "*/5 * * * *", // Valid cron format }, }, ), }, Context: context.Background(), Namespace: "default", }, expectations: []struct { name string failuresCount int }{ // No expectations for valid job }, }, { name: "Multiple issues", config: common.Analyzer{ Client: &kubernetes.Client{ Client: fake.NewSimpleClientset( &batchv1.CronJob{ ObjectMeta: metav1.ObjectMeta{ Name: "multiple-issues", Namespace: "default", }, Spec: batchv1.CronJobSpec{ Schedule: "invalid-cron", StartingDeadlineSeconds: int64Ptr(-60), }, }, ), }, Context: context.Background(), Namespace: "default", }, expectations: []struct { name string failuresCount int }{ { name: "default/multiple-issues", failuresCount: 2, // Two failures: invalid schedule and negative deadline }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { analyzer := CronJobAnalyzer{} results, err := analyzer.Analyze(tt.config) require.NoError(t, err) require.Len(t, results, len(tt.expectations)) // Sort results by name for consistent comparison sort.Slice(results, func(i, j int) bool { return results[i].Name < results[j].Name }) for i, expectation := range tt.expectations { require.Equal(t, expectation.name, results[i].Name) require.Len(t, results[i].Error, expectation.failuresCount) } }) } } func TestCronJobAnalyzerLabelSelector(t *testing.T) { clientSet := fake.NewSimpleClientset( &batchv1.CronJob{ ObjectMeta: metav1.ObjectMeta{ Name: "job-with-label", Namespace: "default", Labels: map[string]string{ "app": "test", }, }, Spec: batchv1.CronJobSpec{ Schedule: "invalid-cron", // This should trigger a failure }, }, &batchv1.CronJob{ ObjectMeta: metav1.ObjectMeta{ Name: "job-without-label", Namespace: "default", }, Spec: batchv1.CronJobSpec{ Schedule: "invalid-cron", // This should trigger a failure }, }, ) // Test with label selector config := common.Analyzer{ Client: &kubernetes.Client{ Client: clientSet, }, Context: context.Background(), Namespace: "default", LabelSelector: "app=test", } analyzer := CronJobAnalyzer{} results, err := analyzer.Analyze(config) require.NoError(t, err) require.Equal(t, 1, len(results)) require.Equal(t, "default/job-with-label", results[0].Name) } func TestCheckCronScheduleIsValid(t *testing.T) { tests := []struct { name string schedule string wantErr bool }{ { name: "Valid schedule - every 5 minutes", schedule: "*/5 * * * *", wantErr: false, }, { name: "Valid schedule - specific time", schedule: "0 2 * * *", wantErr: false, }, { name: "Valid schedule - complex", schedule: "0 0 1,15 * 3", wantErr: false, }, { name: "Invalid schedule - wrong format", schedule: "invalid-cron", wantErr: true, }, { name: "Invalid schedule - too many fields", schedule: "* * * * * *", wantErr: true, }, { name: "Invalid schedule - empty string", schedule: "", wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { _, err := CheckCronScheduleIsValid(tt.schedule) if tt.wantErr { require.Error(t, err) } else { require.NoError(t, err) } }) } } ================================================ FILE: pkg/analyzer/deployment.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package analyzer import ( "context" "fmt" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" "github.com/k8sgpt-ai/k8sgpt/pkg/util" ) // DeploymentAnalyzer is an analyzer that checks for misconfigured Deployments type DeploymentAnalyzer struct { } // Analyze scans all namespaces for Deployments with misconfigurations func (d DeploymentAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) { kind := "Deployment" apiDoc := kubernetes.K8sApiReference{ Kind: kind, ApiVersion: schema.GroupVersion{ Group: "apps", Version: "v1", }, OpenapiSchema: a.OpenapiSchema, } AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{ "analyzer_name": kind, }) deployments, err := a.Client.GetClient().AppsV1().Deployments(a.Namespace).List(context.Background(), v1.ListOptions{LabelSelector: a.LabelSelector}) if err != nil { return nil, err } var preAnalysis = map[string]common.PreAnalysis{} for _, deployment := range deployments.Items { var failures []common.Failure if *deployment.Spec.Replicas != deployment.Status.ReadyReplicas { if deployment.Status.Replicas > *deployment.Spec.Replicas { doc := apiDoc.GetApiDocV2("spec.replicas") failures = append(failures, common.Failure{ Text: fmt.Sprintf("Deployment %s/%s has %d replicas in spec but %d replicas in status because status field is not updated yet after scaling and %d replicas are available with status running", deployment.Namespace, deployment.Name, *deployment.Spec.Replicas, deployment.Status.Replicas, deployment.Status.ReadyReplicas), KubernetesDoc: doc, Sensitive: []common.Sensitive{ { Unmasked: deployment.Namespace, Masked: util.MaskString(deployment.Namespace), }, { Unmasked: deployment.Name, Masked: util.MaskString(deployment.Name), }, }}) } else { doc := apiDoc.GetApiDocV2("spec.replicas") failures = append(failures, common.Failure{ Text: fmt.Sprintf("Deployment %s/%s has %d replicas but %d are available with status running", deployment.Namespace, deployment.Name, *deployment.Spec.Replicas, deployment.Status.ReadyReplicas), KubernetesDoc: doc, Sensitive: []common.Sensitive{ { Unmasked: deployment.Namespace, Masked: util.MaskString(deployment.Namespace), }, { Unmasked: deployment.Name, Masked: util.MaskString(deployment.Name), }, }}) } } if len(failures) > 0 { preAnalysis[fmt.Sprintf("%s/%s", deployment.Namespace, deployment.Name)] = common.PreAnalysis{ FailureDetails: failures, Deployment: deployment, } AnalyzerErrorsMetric.WithLabelValues(kind, deployment.Name, deployment.Namespace).Set(float64(len(failures))) } } for key, value := range preAnalysis { var currentAnalysis = common.Result{ Kind: kind, Name: key, Error: value.FailureDetails, } a.Results = append(a.Results, currentAnalysis) } return a.Results, nil } ================================================ FILE: pkg/analyzer/deployment_test.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package analyzer import ( "context" "testing" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" "github.com/magiconair/properties/assert" appsv1 "k8s.io/api/apps/v1" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/fake" ) func TestDeploymentAnalyzer(t *testing.T) { clientset := fake.NewSimpleClientset(&appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "default", }, Spec: appsv1.DeploymentSpec{ Replicas: func() *int32 { i := int32(3); return &i }(), Template: v1.PodTemplateSpec{ Spec: v1.PodSpec{ Containers: []v1.Container{ { Name: "example-container", Image: "nginx", Ports: []v1.ContainerPort{ { ContainerPort: 80, }, }, }, }, }, }, }, Status: appsv1.DeploymentStatus{ Replicas: 2, AvailableReplicas: 1, }, }) config := common.Analyzer{ Client: &kubernetes.Client{ Client: clientset, }, Context: context.Background(), Namespace: "default", } deploymentAnalyzer := DeploymentAnalyzer{} analysisResults, err := deploymentAnalyzer.Analyze(config) if err != nil { t.Error(err) } assert.Equal(t, len(analysisResults), 1) assert.Equal(t, analysisResults[0].Kind, "Deployment") assert.Equal(t, analysisResults[0].Name, "default/example") } func TestDeploymentAnalyzerNamespaceFiltering(t *testing.T) { clientset := fake.NewSimpleClientset( &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "default", }, Spec: appsv1.DeploymentSpec{ Replicas: func() *int32 { i := int32(3); return &i }(), Template: v1.PodTemplateSpec{ Spec: v1.PodSpec{ Containers: []v1.Container{ { Name: "example-container", Image: "nginx", Ports: []v1.ContainerPort{ { ContainerPort: 80, }, }, }, }, }, }, }, Status: appsv1.DeploymentStatus{ Replicas: 2, AvailableReplicas: 1, }, }, &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "other-namespace", }, Spec: appsv1.DeploymentSpec{ Replicas: func() *int32 { i := int32(3); return &i }(), Template: v1.PodTemplateSpec{ Spec: v1.PodSpec{ Containers: []v1.Container{ { Name: "example-container", Image: "nginx", Ports: []v1.ContainerPort{ { ContainerPort: 80, }, }, }, }, }, }, }, Status: appsv1.DeploymentStatus{ Replicas: 2, AvailableReplicas: 1, }, }, ) config := common.Analyzer{ Client: &kubernetes.Client{ Client: clientset, }, Context: context.Background(), Namespace: "default", } deploymentAnalyzer := DeploymentAnalyzer{} analysisResults, err := deploymentAnalyzer.Analyze(config) if err != nil { t.Error(err) } assert.Equal(t, len(analysisResults), 1) assert.Equal(t, analysisResults[0].Kind, "Deployment") assert.Equal(t, analysisResults[0].Name, "default/example") } func TestDeploymentAnalyzerLabelSelectorFiltering(t *testing.T) { clientset := fake.NewSimpleClientset( &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "default", Labels: map[string]string{ "app": "deployment", }, }, Spec: appsv1.DeploymentSpec{ Replicas: func() *int32 { i := int32(3); return &i }(), Template: v1.PodTemplateSpec{ Spec: v1.PodSpec{ Containers: []v1.Container{}, }, }, }, }, &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: "example2", Namespace: "default", }, Spec: appsv1.DeploymentSpec{ Replicas: func() *int32 { i := int32(3); return &i }(), Template: v1.PodTemplateSpec{ Spec: v1.PodSpec{ Containers: []v1.Container{}, }, }, }, }, ) config := common.Analyzer{ Client: &kubernetes.Client{ Client: clientset, }, Context: context.Background(), Namespace: "default", LabelSelector: "app=deployment", } deploymentAnalyzer := DeploymentAnalyzer{} analysisResults, err := deploymentAnalyzer.Analyze(config) if err != nil { t.Error(err) } assert.Equal(t, len(analysisResults), 1) } ================================================ FILE: pkg/analyzer/events_test.go ================================================ package analyzer_test import ( "context" "errors" "testing" "time" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/fake" ) func FetchLatestEvent(ctx context.Context, client kubernetes.Interface, namespace, eventName string) (*v1.Event, error) { // List events in the specified namespace events, err := client.CoreV1().Events(namespace).List(ctx, metav1.ListOptions{}) if err != nil { return nil, err } var latestEvent *v1.Event for _, event := range events.Items { // Check if the event name matches the requested name (partial match) if eventName == "" || event.Name == eventName { if latestEvent == nil || event.LastTimestamp.Time.After(latestEvent.LastTimestamp.Time) { latestEvent = &event } } } // If no matching event is found, return an error if latestEvent == nil { return nil, errors.New("no matching events found") } return latestEvent, nil } func TestFetchLatestEvent(t *testing.T) { fakeClient := fake.NewSimpleClientset() // Simulating events with different timestamps event1 := &v1.Event{ ObjectMeta: metav1.ObjectMeta{ Name: "test-event-1", Namespace: "default", }, LastTimestamp: metav1.Time{Time: time.Now()}, } event2 := &v1.Event{ ObjectMeta: metav1.ObjectMeta{ Name: "test-event-2", Namespace: "default", }, LastTimestamp: metav1.Time{Time: time.Now().Add(-time.Hour)}, // event1 should be fetched as it's newer } // ✅ Explicitly ensure namespace exists _, err := fakeClient.CoreV1().Namespaces().Create(context.TODO(), &v1.Namespace{ ObjectMeta: metav1.ObjectMeta{Name: "default"}, }, metav1.CreateOptions{}) if err != nil { t.Fatalf("Failed to create namespace: %v", err) } // ✅ Ensure events are properly created and stored in the fake client _, err = fakeClient.CoreV1().Events("default").Create(context.TODO(), event1, metav1.CreateOptions{}) if err != nil { t.Fatalf("Failed to create event1: %v", err) } _, err = fakeClient.CoreV1().Events("default").Create(context.TODO(), event2, metav1.CreateOptions{}) if err != nil { t.Fatalf("Failed to create event2: %v", err) } // 🔍 Debug: Check if events exist before running FetchLatestEvent storedEvents, _ := fakeClient.CoreV1().Events("default").List(context.TODO(), metav1.ListOptions{}) if len(storedEvents.Items) == 0 { t.Fatal("No events were found in the fake client. Ensure event creation is working correctly.") } // Test cases tests := []struct { name string namespace string nameToFind string expected *v1.Event shouldFail bool }{ { name: "Valid case - fetch the latest event", namespace: "default", nameToFind: "test-event-1", // Match exact event name expected: event1, // event1 has the latest timestamp shouldFail: false, }, { name: "Nonexistent event", namespace: "default", nameToFind: "nonexistent-event", // Should not exist expected: nil, shouldFail: true, }, { name: "Nonexistent namespace", namespace: "nonexistent-namespace", // Namespace doesn't exist nameToFind: "test-event", expected: nil, shouldFail: true, }, } // Run tests for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Call the function to fetch the latest event event, err := FetchLatestEvent(context.TODO(), fakeClient, tt.namespace, tt.nameToFind) // Handle the expected outcomes based on the test case if tt.shouldFail { if err == nil { t.Error("Expected an error, but got nil") } if event != nil { t.Errorf("Expected nil event, but got event: %s", event.Name) } } else { if err != nil { t.Errorf("Expected no error, but got %v", err) } if event != nil && event.Name != tt.expected.Name { t.Errorf("Expected event name %s, got %s", tt.expected.Name, event.Name) } } }) } } ================================================ FILE: pkg/analyzer/gateway.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package analyzer import ( "fmt" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/util" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ctrl "sigs.k8s.io/controller-runtime/pkg/client" gtwapi "sigs.k8s.io/gateway-api/apis/v1" ) type GatewayAnalyzer struct{} // Gateway analyser will analyse all different Kinds and search for missing object dependencies func (GatewayAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) { kind := "Gateway" AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{ "analyzer_name": kind, }) gtwList := >wapi.GatewayList{} gc := >wapi.GatewayClass{} client := a.Client.CtrlClient err := gtwapi.AddToScheme(client.Scheme()) if err != nil { return nil, err } labelSelector := util.LabelStrToSelector(a.LabelSelector) if err := client.List(a.Context, gtwList, &ctrl.ListOptions{LabelSelector: labelSelector}); err != nil { return nil, err } var preAnalysis = map[string]common.PreAnalysis{} // Find all unhealthy gateway Classes for _, gtw := range gtwList.Items { var failures []common.Failure gtwName := gtw.GetName() gtwNamespace := gtw.GetNamespace() // Check if gatewayclass exists err := client.Get(a.Context, ctrl.ObjectKey{Namespace: gtwNamespace, Name: string(gtw.Spec.GatewayClassName)}, gc, &ctrl.GetOptions{}) if errors.IsNotFound(err) { failures = append(failures, common.Failure{ Text: fmt.Sprintf( "Gateway uses the GatewayClass %s which does not exist.", gtw.Spec.GatewayClassName, ), Sensitive: []common.Sensitive{ { Unmasked: string(gtw.Spec.GatewayClassName), Masked: util.MaskString(string(gtw.Spec.GatewayClassName)), }, }, }) } // Check only the current conditions // TODO: maybe check other statuses Listeners, addresses? if gtw.Status.Conditions[0].Status != metav1.ConditionTrue { failures = append(failures, common.Failure{ Text: fmt.Sprintf("Gateway '%s/%s' is not accepted. Message: '%s'.", gtwNamespace, gtwName, gtw.Status.Conditions[0].Message, ), Sensitive: []common.Sensitive{ { Unmasked: gtwNamespace, Masked: util.MaskString(gtwNamespace), }, { Unmasked: gtwName, Masked: util.MaskString(gtwName), }, }, }) } if len(failures) > 0 { preAnalysis[fmt.Sprintf("%s/%s", gtwNamespace, gtwName)] = common.PreAnalysis{ Gateway: gtw, FailureDetails: failures, } AnalyzerErrorsMetric.WithLabelValues(kind, gtwName, gtwNamespace).Set(float64(len(failures))) } } for key, value := range preAnalysis { var currentAnalysis = common.Result{ Kind: kind, Name: key, Error: value.FailureDetails, } a.Results = append(a.Results, currentAnalysis) } return a.Results, nil } ================================================ FILE: pkg/analyzer/gateway_test.go ================================================ package analyzer import ( "context" "testing" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" "github.com/magiconair/properties/assert" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/kubernetes/scheme" fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" gtwapi "sigs.k8s.io/gateway-api/apis/v1" ) func BuildGatewayClass(name string) gtwapi.GatewayClass { GatewayClass := gtwapi.GatewayClass{} GatewayClass.Name = name // Namespace is not needed outside of this test, GatewayClass is cluster-scoped GatewayClass.Namespace = "default" GatewayClass.Spec.ControllerName = "gateway.fooproxy.io/gatewayclass-controller" return GatewayClass } func BuildGateway(className gtwapi.ObjectName, status metav1.ConditionStatus, labels map[string]string) gtwapi.Gateway { Gateway := gtwapi.Gateway{} Gateway.Name = "foobar" Gateway.Namespace = "default" if labels != nil { Gateway.Labels = labels } Gateway.Spec.GatewayClassName = className Gateway.Spec.Listeners = []gtwapi.Listener{ { Name: "proxy", Port: 80, Protocol: gtwapi.HTTPProtocolType, }, } Condition := metav1.Condition{ Type: "Accepted", Status: status, Message: "An expected message", Reason: "Test", } Gateway.Status.Conditions = []metav1.Condition{Condition} return Gateway } func TestGatewayAnalyzer(t *testing.T) { ClassName := gtwapi.ObjectName("exists") AcceptedStatus := metav1.ConditionTrue GatewayClass := BuildGatewayClass(string(ClassName)) Gateway := BuildGateway(ClassName, AcceptedStatus, nil) // Create a Gateway Analyzer instance with the fake client scheme := scheme.Scheme err := gtwapi.Install(scheme) if err != nil { t.Error(err) } err = apiextensionsv1.AddToScheme(scheme) if err != nil { t.Error(err) } objects := []runtime.Object{ &Gateway, &GatewayClass, } fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(objects...).Build() analyzerInstance := GatewayAnalyzer{} config := common.Analyzer{ Client: &kubernetes.Client{ CtrlClient: fakeClient, }, Context: context.Background(), Namespace: "default", } analysisResults, err := analyzerInstance.Analyze(config) if err != nil { t.Error(err) } assert.Equal(t, len(analysisResults), 0) } func TestMissingClassGatewayAnalyzer(t *testing.T) { ClassName := gtwapi.ObjectName("non-existed") AcceptedStatus := metav1.ConditionTrue Gateway := BuildGateway(ClassName, AcceptedStatus, nil) // Create a Gateway Analyzer instance with the fake client scheme := scheme.Scheme err := gtwapi.Install(scheme) if err != nil { t.Error(err) } err = apiextensionsv1.AddToScheme(scheme) if err != nil { t.Error(err) } objects := []runtime.Object{ &Gateway, } fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(objects...).Build() analyzerInstance := GatewayAnalyzer{} config := common.Analyzer{ Client: &kubernetes.Client{ CtrlClient: fakeClient, }, Context: context.Background(), Namespace: "default", } analysisResults, err := analyzerInstance.Analyze(config) if err != nil { t.Error(err) } assert.Equal(t, len(analysisResults), 1) } func TestStatusGatewayAnalyzer(t *testing.T) { ClassName := gtwapi.ObjectName("exists") AcceptedStatus := metav1.ConditionUnknown GatewayClass := BuildGatewayClass(string(ClassName)) Gateway := BuildGateway(ClassName, AcceptedStatus, nil) // Create a Gateway Analyzer instance with the fake client scheme := scheme.Scheme err := gtwapi.Install(scheme) if err != nil { t.Error(err) } err = apiextensionsv1.AddToScheme(scheme) if err != nil { t.Error(err) } objects := []runtime.Object{ &Gateway, &GatewayClass, } fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(objects...).Build() analyzerInstance := GatewayAnalyzer{} config := common.Analyzer{ Client: &kubernetes.Client{ CtrlClient: fakeClient, }, Context: context.Background(), Namespace: "default", } analysisResults, err := analyzerInstance.Analyze(config) if err != nil { t.Error(err) } var errorFound bool want := "Gateway 'default/foobar' is not accepted. Message: 'An expected message'." for _, analysis := range analysisResults { for _, got := range analysis.Error { if want == got.Text { errorFound = true } } if errorFound { break } } if !errorFound { t.Errorf("Expected message, <%v> , not found in Gateway's analysis results", want) } } func TestGatewayAnalyzerLabelSelectorFiltering(t *testing.T) { ClassName := gtwapi.ObjectName("non-existed") AcceptedStatus := metav1.ConditionTrue Gateway := BuildGateway(ClassName, AcceptedStatus, map[string]string{"app": "gateway"}) scheme := scheme.Scheme err := gtwapi.Install(scheme) if err != nil { t.Error(err) } err = apiextensionsv1.AddToScheme(scheme) if err != nil { t.Error(err) } objects := []runtime.Object{ &Gateway, } fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(objects...).Build() analyzerInstance := GatewayAnalyzer{} // without label selector should return 1 result config := common.Analyzer{ Client: &kubernetes.Client{ CtrlClient: fakeClient, }, Context: context.Background(), Namespace: "default", } analysisResults, err := analyzerInstance.Analyze(config) if err != nil { t.Error(err) } assert.Equal(t, len(analysisResults), 1) // with label selector should return 1 result config = common.Analyzer{ Client: &kubernetes.Client{ CtrlClient: fakeClient, }, Context: context.Background(), Namespace: "default", LabelSelector: "app=gateway", } analysisResults, err = analyzerInstance.Analyze(config) if err != nil { t.Error(err) } assert.Equal(t, len(analysisResults), 1) // with wrong label selector should return 0 result config = common.Analyzer{ Client: &kubernetes.Client{ CtrlClient: fakeClient, }, Context: context.Background(), Namespace: "default", LabelSelector: "app=wrong", } analysisResults, err = analyzerInstance.Analyze(config) if err != nil { t.Error(err) } assert.Equal(t, len(analysisResults), 0) } ================================================ FILE: pkg/analyzer/gatewayclass.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package analyzer import ( "fmt" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/util" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ctrl "sigs.k8s.io/controller-runtime/pkg/client" gtwapi "sigs.k8s.io/gateway-api/apis/v1" ) type GatewayClassAnalyzer struct{} // Gateway analyser will analyse all different Kinds and search for missing object dependencies func (GatewayClassAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) { kind := "GatewayClass" AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{ "analyzer_name": kind, }) gcList := >wapi.GatewayClassList{} client := a.Client.CtrlClient err := gtwapi.AddToScheme(client.Scheme()) if err != nil { return nil, err } labelSelector := util.LabelStrToSelector(a.LabelSelector) if err := client.List(a.Context, gcList, &ctrl.ListOptions{LabelSelector: labelSelector}); err != nil { return nil, err } var preAnalysis = map[string]common.PreAnalysis{} // Find all unhealthy gateway Classes for _, gc := range gcList.Items { var failures []common.Failure gcName := gc.GetName() // Check only the current condition if gc.Status.Conditions[0].Status != metav1.ConditionTrue { failures = append(failures, common.Failure{ Text: fmt.Sprintf( "GatewayClass '%s' with a controller name '%s' is not accepted. Message: '%s'.", gcName, gc.Spec.ControllerName, gc.Status.Conditions[0].Message, ), Sensitive: []common.Sensitive{ { Unmasked: gcName, Masked: util.MaskString(gcName), }, }, }) } if len(failures) > 0 { preAnalysis[gcName] = common.PreAnalysis{ GatewayClass: gc, FailureDetails: failures, } AnalyzerErrorsMetric.WithLabelValues(kind, gcName, "").Set(float64(len(failures))) } } for key, value := range preAnalysis { var currentAnalysis = common.Result{ Kind: kind, Name: key, Error: value.FailureDetails, } a.Results = append(a.Results, currentAnalysis) } return a.Results, nil } ================================================ FILE: pkg/analyzer/gatewayclass_test.go ================================================ package analyzer import ( "context" "testing" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" "github.com/stretchr/testify/assert" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/scheme" fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" gtwapi "sigs.k8s.io/gateway-api/apis/v1" ) // Testing with the fake dynamic client if GatewayClasses have an accepted status func TestGatewayClassAnalyzer(t *testing.T) { GatewayClass := >wapi.GatewayClass{} GatewayClass.Name = "foobar" GatewayClass.Spec.ControllerName = "gateway.fooproxy.io/gatewayclass-controller" // Initialize Conditions slice before setting properties BadCondition := metav1.Condition{ Type: "Accepted", Status: "Uknown", Message: "Waiting for controller", Reason: "Pending", } GatewayClass.Status.Conditions = []metav1.Condition{BadCondition} // Create a GatewayClassAnalyzer instance with the fake client scheme := scheme.Scheme err := gtwapi.Install(scheme) if err != nil { t.Error(err) } err = apiextensionsv1.AddToScheme(scheme) if err != nil { t.Error(err) } fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(GatewayClass).Build() analyzerInstance := GatewayClassAnalyzer{} config := common.Analyzer{ Client: &kubernetes.Client{ CtrlClient: fakeClient, }, Context: context.Background(), Namespace: "default", } analysisResults, err := analyzerInstance.Analyze(config) if err != nil { t.Error(err) } assert.Equal(t, len(analysisResults), 1) } func TestGatewayClassAnalyzerLabelSelectorFiltering(t *testing.T) { condition := metav1.Condition{ Type: "Accepted", Status: "Ready", Message: "Ready", Reason: "Ready", } // Create two GatewayClasses with different labels GatewayClass := >wapi.GatewayClass{} GatewayClass.Name = "foobar" GatewayClass.Spec.ControllerName = "gateway.fooproxy.io/gatewayclass-controller" GatewayClass.Labels = map[string]string{"app": "gatewayclass"} GatewayClass.Status.Conditions = []metav1.Condition{condition} GatewayClass2 := >wapi.GatewayClass{} GatewayClass2.Name = "foobar2" GatewayClass2.Spec.ControllerName = "gateway.fooproxy.io/gatewayclass-controller" GatewayClass2.Status.Conditions = []metav1.Condition{condition} scheme := scheme.Scheme err := gtwapi.Install(scheme) if err != nil { t.Error(err) } err = apiextensionsv1.AddToScheme(scheme) if err != nil { t.Error(err) } fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(GatewayClass, GatewayClass2).Build() analyzerInstance := GatewayClassAnalyzer{} config := common.Analyzer{ Client: &kubernetes.Client{ CtrlClient: fakeClient, }, Context: context.Background(), Namespace: "default", LabelSelector: "app=gatewayclass", } analysisResults, err := analyzerInstance.Analyze(config) if err != nil { t.Error(err) } assert.Equal(t, len(analysisResults), 1) } ================================================ FILE: pkg/analyzer/hpa.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package analyzer import ( "fmt" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" "github.com/k8sgpt-ai/k8sgpt/pkg/util" appsv1 "k8s.io/api/apps/v1" autoscalingv2 "k8s.io/api/autoscaling/v2" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" ) type HpaAnalyzer struct{} func (HpaAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) { kind := "HorizontalPodAutoscaler" apiDoc := kubernetes.K8sApiReference{ Kind: kind, ApiVersion: schema.GroupVersion{ Group: "autoscaling", Version: "v2", }, OpenapiSchema: a.OpenapiSchema, } AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{ "analyzer_name": kind, }) list, err := a.Client.GetClient().AutoscalingV2().HorizontalPodAutoscalers(a.Namespace).List(a.Context, metav1.ListOptions{LabelSelector: a.LabelSelector}) if err != nil { return nil, err } var preAnalysis = map[string]common.PreAnalysis{} for _, hpa := range list.Items { var failures []common.Failure //check the error from status field conditions := hpa.Status.Conditions for _, condition := range conditions { // https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale-walkthrough/#appendix-horizontal-pod-autoscaler-status-conditions switch condition.Type { case autoscalingv2.ScalingLimited: if condition.Status == corev1.ConditionTrue { failures = append(failures, common.Failure{ Text: condition.Message, Sensitive: []common.Sensitive{}, }) } default: if condition.Status == corev1.ConditionFalse { failures = append(failures, common.Failure{ Text: condition.Message, Sensitive: []common.Sensitive{}, }) } } } // check ScaleTargetRef exist scaleTargetRef := hpa.Spec.ScaleTargetRef var podInfo PodInfo switch scaleTargetRef.Kind { case "Deployment": deployment, err := a.Client.GetClient().AppsV1().Deployments(hpa.Namespace).Get(a.Context, scaleTargetRef.Name, metav1.GetOptions{}) if err == nil { podInfo = DeploymentInfo{deployment} } case "ReplicationController": rc, err := a.Client.GetClient().CoreV1().ReplicationControllers(hpa.Namespace).Get(a.Context, scaleTargetRef.Name, metav1.GetOptions{}) if err == nil { podInfo = ReplicationControllerInfo{rc} } case "ReplicaSet": rs, err := a.Client.GetClient().AppsV1().ReplicaSets(hpa.Namespace).Get(a.Context, scaleTargetRef.Name, metav1.GetOptions{}) if err == nil { podInfo = ReplicaSetInfo{rs} } case "StatefulSet": ss, err := a.Client.GetClient().AppsV1().StatefulSets(hpa.Namespace).Get(a.Context, scaleTargetRef.Name, metav1.GetOptions{}) if err == nil { podInfo = StatefulSetInfo{ss} } default: failures = append(failures, common.Failure{ Text: fmt.Sprintf("HorizontalPodAutoscaler uses %s as ScaleTargetRef which is not an option.", scaleTargetRef.Kind), Sensitive: []common.Sensitive{}, }) } if podInfo == nil { doc := apiDoc.GetApiDocV2("spec.scaleTargetRef") failures = append(failures, common.Failure{ Text: fmt.Sprintf("HorizontalPodAutoscaler uses %s/%s as ScaleTargetRef which does not exist.", scaleTargetRef.Kind, scaleTargetRef.Name), KubernetesDoc: doc, Sensitive: []common.Sensitive{ { Unmasked: scaleTargetRef.Name, Masked: util.MaskString(scaleTargetRef.Name), }, }, }) } else { containers := len(podInfo.GetPodSpec().Containers) for _, container := range podInfo.GetPodSpec().Containers { if container.Resources.Requests == nil || container.Resources.Limits == nil { containers-- } } if containers <= 0 { doc := apiDoc.GetApiDocV2("spec.scaleTargetRef.kind") failures = append(failures, common.Failure{ Text: fmt.Sprintf("%s %s/%s does not have resource configured.", scaleTargetRef.Kind, a.Namespace, scaleTargetRef.Name), KubernetesDoc: doc, Sensitive: []common.Sensitive{ { Unmasked: scaleTargetRef.Name, Masked: util.MaskString(scaleTargetRef.Name), }, }, }) } } if len(failures) > 0 { preAnalysis[fmt.Sprintf("%s/%s", hpa.Namespace, hpa.Name)] = common.PreAnalysis{ HorizontalPodAutoscalers: hpa, FailureDetails: failures, } AnalyzerErrorsMetric.WithLabelValues(kind, hpa.Name, hpa.Namespace).Set(float64(len(failures))) } } for key, value := range preAnalysis { var currentAnalysis = common.Result{ Kind: kind, Name: key, Error: value.FailureDetails, } parent, found := util.GetParent(a.Client, value.HorizontalPodAutoscalers.ObjectMeta) if found { currentAnalysis.ParentObject = parent } a.Results = append(a.Results, currentAnalysis) } return a.Results, nil } type PodInfo interface { GetPodSpec() corev1.PodSpec } type DeploymentInfo struct { *appsv1.Deployment } func (d DeploymentInfo) GetPodSpec() corev1.PodSpec { return d.Spec.Template.Spec } // define a structure for ReplicationController type ReplicationControllerInfo struct { *corev1.ReplicationController } func (rc ReplicationControllerInfo) GetPodSpec() corev1.PodSpec { return rc.Spec.Template.Spec } // define a structure for ReplicaSet type ReplicaSetInfo struct { *appsv1.ReplicaSet } func (rs ReplicaSetInfo) GetPodSpec() corev1.PodSpec { return rs.Spec.Template.Spec } // define a structure for StatefulSet type StatefulSetInfo struct { *appsv1.StatefulSet } // implement PodInfo for StatefulSetInfo func (ss StatefulSetInfo) GetPodSpec() corev1.PodSpec { return ss.Spec.Template.Spec } ================================================ FILE: pkg/analyzer/hpa_test.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package analyzer import ( "context" "strings" "testing" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" "github.com/magiconair/properties/assert" appsv1 "k8s.io/api/apps/v1" autoscalingv2 "k8s.io/api/autoscaling/v2" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/fake" ) func TestHPAAnalyzer(t *testing.T) { clientset := fake.NewSimpleClientset( &autoscalingv2.HorizontalPodAutoscaler{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "default", Annotations: map[string]string{}, }, }) hpaAnalyzer := HpaAnalyzer{} config := common.Analyzer{ Client: &kubernetes.Client{ Client: clientset, }, Context: context.Background(), Namespace: "default", } analysisResults, err := hpaAnalyzer.Analyze(config) if err != nil { t.Error(err) } assert.Equal(t, len(analysisResults), 1) } func TestHPAAnalyzerWithMultipleHPA(t *testing.T) { clientset := fake.NewSimpleClientset( &autoscalingv2.HorizontalPodAutoscaler{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "default", Annotations: map[string]string{}, }, }, &autoscalingv2.HorizontalPodAutoscaler{ ObjectMeta: metav1.ObjectMeta{ Name: "example-2", Namespace: "default", Annotations: map[string]string{}, }, }, ) hpaAnalyzer := HpaAnalyzer{} config := common.Analyzer{ Client: &kubernetes.Client{ Client: clientset, }, Context: context.Background(), Namespace: "default", } analysisResults, err := hpaAnalyzer.Analyze(config) if err != nil { t.Error(err) } assert.Equal(t, len(analysisResults), 2) } func TestHPAAnalyzerWithUnsuportedScaleTargetRef(t *testing.T) { clientset := fake.NewSimpleClientset( &autoscalingv2.HorizontalPodAutoscaler{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "default", Annotations: map[string]string{}, }, Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ Kind: "unsupported", }, }, }) hpaAnalyzer := HpaAnalyzer{} config := common.Analyzer{ Client: &kubernetes.Client{ Client: clientset, }, Context: context.Background(), Namespace: "default", } analysisResults, err := hpaAnalyzer.Analyze(config) if err != nil { t.Error(err) } var errorFound bool for _, analysis := range analysisResults { for _, err := range analysis.Error { if strings.Contains(err.Text, "which is not an option.") { errorFound = true break } } if errorFound { break } } if !errorFound { t.Error("expected error 'does not possible option.' not found in analysis results") } } func TestHPAAnalyzerWithNonExistentScaleTargetRef(t *testing.T) { clientset := fake.NewSimpleClientset( &autoscalingv2.HorizontalPodAutoscaler{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "default", Annotations: map[string]string{}, }, Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ Kind: "Deployment", Name: "non-existent", }, }, }) hpaAnalyzer := HpaAnalyzer{} config := common.Analyzer{ Client: &kubernetes.Client{ Client: clientset, }, Context: context.Background(), Namespace: "default", } analysisResults, err := hpaAnalyzer.Analyze(config) if err != nil { t.Error(err) } var errorFound bool for _, analysis := range analysisResults { for _, err := range analysis.Error { if strings.Contains(err.Text, "does not exist.") { errorFound = true break } } if errorFound { break } } if !errorFound { t.Error("expected error 'does not exist.' not found in analysis results") } } func TestHPAAnalyzerWithExistingScaleTargetRefAsDeployment(t *testing.T) { clientset := fake.NewSimpleClientset( &autoscalingv2.HorizontalPodAutoscaler{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "default", Annotations: map[string]string{}, }, Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ Kind: "Deployment", Name: "example", }, }, }, &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "default", Annotations: map[string]string{}, }, Spec: appsv1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "example", Image: "nginx", Resources: corev1.ResourceRequirements{ Requests: corev1.ResourceList{ "cpu": resource.MustParse("100m"), "memory": resource.MustParse("128Mi"), }, Limits: corev1.ResourceList{ "cpu": resource.MustParse("200m"), "memory": resource.MustParse("256Mi"), }, }, }, }, }, }, }, }, ) hpaAnalyzer := HpaAnalyzer{} config := common.Analyzer{ Client: &kubernetes.Client{ Client: clientset, }, Context: context.Background(), Namespace: "default", } analysisResults, err := hpaAnalyzer.Analyze(config) if err != nil { t.Error(err) } for _, analysis := range analysisResults { assert.Equal(t, len(analysis.Error), 0) } } func TestHPAAnalyzerWithExistingScaleTargetRefAsReplicationController(t *testing.T) { clientset := fake.NewSimpleClientset( &autoscalingv2.HorizontalPodAutoscaler{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "default", Annotations: map[string]string{}, }, Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ Kind: "ReplicationController", Name: "example", }, }, }, &corev1.ReplicationController{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "default", Annotations: map[string]string{}, }, Spec: corev1.ReplicationControllerSpec{ Template: &corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "example", Image: "nginx", Resources: corev1.ResourceRequirements{ Requests: corev1.ResourceList{ "cpu": resource.MustParse("100m"), "memory": resource.MustParse("128Mi"), }, Limits: corev1.ResourceList{ "cpu": resource.MustParse("200m"), "memory": resource.MustParse("256Mi"), }, }, }, }, }, }, }, }, ) hpaAnalyzer := HpaAnalyzer{} config := common.Analyzer{ Client: &kubernetes.Client{ Client: clientset, }, Context: context.Background(), Namespace: "default", } analysisResults, err := hpaAnalyzer.Analyze(config) if err != nil { t.Error(err) } for _, analysis := range analysisResults { assert.Equal(t, len(analysis.Error), 0) } } func TestHPAAnalyzerWithExistingScaleTargetRefAsReplicaSet(t *testing.T) { clientset := fake.NewSimpleClientset( &autoscalingv2.HorizontalPodAutoscaler{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "default", Annotations: map[string]string{}, }, Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ Kind: "ReplicaSet", Name: "example", }, }, }, &appsv1.ReplicaSet{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "default", Annotations: map[string]string{}, }, Spec: appsv1.ReplicaSetSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "example", Image: "nginx", Resources: corev1.ResourceRequirements{ Requests: corev1.ResourceList{ "cpu": resource.MustParse("100m"), "memory": resource.MustParse("128Mi"), }, Limits: corev1.ResourceList{ "cpu": resource.MustParse("200m"), "memory": resource.MustParse("256Mi"), }, }, }, }, }, }, }, }, ) hpaAnalyzer := HpaAnalyzer{} config := common.Analyzer{ Client: &kubernetes.Client{ Client: clientset, }, Context: context.Background(), Namespace: "default", } analysisResults, err := hpaAnalyzer.Analyze(config) if err != nil { t.Error(err) } for _, analysis := range analysisResults { assert.Equal(t, len(analysis.Error), 0) } } func TestHPAAnalyzerWithExistingScaleTargetRefAsStatefulSet(t *testing.T) { clientset := fake.NewSimpleClientset( &autoscalingv2.HorizontalPodAutoscaler{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "default", Annotations: map[string]string{}, }, Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ Kind: "StatefulSet", Name: "example", }, }, }, &appsv1.StatefulSet{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "default", Annotations: map[string]string{}, }, Spec: appsv1.StatefulSetSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "example", Image: "nginx", Resources: corev1.ResourceRequirements{ Requests: corev1.ResourceList{ "cpu": resource.MustParse("100m"), "memory": resource.MustParse("128Mi"), }, Limits: corev1.ResourceList{ "cpu": resource.MustParse("200m"), "memory": resource.MustParse("256Mi"), }, }, }, }, }, }, }, }, ) hpaAnalyzer := HpaAnalyzer{} config := common.Analyzer{ Client: &kubernetes.Client{ Client: clientset, }, Context: context.Background(), Namespace: "default", } analysisResults, err := hpaAnalyzer.Analyze(config) if err != nil { t.Error(err) } for _, analysis := range analysisResults { assert.Equal(t, len(analysis.Error), 0) } } func TestHPAAnalyzerWithExistingScaleTargetRefWithoutSpecifyingResources(t *testing.T) { clientset := fake.NewSimpleClientset( &autoscalingv2.HorizontalPodAutoscaler{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "default", Annotations: map[string]string{}, }, Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ Kind: "Deployment", Name: "example", }, }, }, &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "default", Annotations: map[string]string{}, }, Spec: appsv1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "example", Image: "nginx", }, }, }, }, }, }, ) hpaAnalyzer := HpaAnalyzer{} config := common.Analyzer{ Client: &kubernetes.Client{ Client: clientset, }, Context: context.Background(), Namespace: "default", } analysisResults, err := hpaAnalyzer.Analyze(config) if err != nil { t.Error(err) } var errorFound bool for _, analysis := range analysisResults { for _, err := range analysis.Error { if strings.Contains(err.Text, "does not have resource configured.") { errorFound = true break } if errorFound { break } } if !errorFound { t.Error("expected error 'does not have resource configured.' not found in analysis results") } } } func TestHPAAnalyzerNamespaceFiltering(t *testing.T) { clientset := fake.NewSimpleClientset( &autoscalingv2.HorizontalPodAutoscaler{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "default", Annotations: map[string]string{}, }, }, &autoscalingv2.HorizontalPodAutoscaler{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "other-namespace", Annotations: map[string]string{}, }, }) hpaAnalyzer := HpaAnalyzer{} config := common.Analyzer{ Client: &kubernetes.Client{ Client: clientset, }, Context: context.Background(), Namespace: "default", } analysisResults, err := hpaAnalyzer.Analyze(config) if err != nil { t.Error(err) } assert.Equal(t, len(analysisResults), 1) } func TestHPAAnalyzerLabelSelectorFiltering(t *testing.T) { clientset := fake.NewSimpleClientset( &autoscalingv2.HorizontalPodAutoscaler{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "default", Labels: map[string]string{ "app": "hpa", }, }, }, &autoscalingv2.HorizontalPodAutoscaler{ ObjectMeta: metav1.ObjectMeta{ Name: "example2", Namespace: "default", }, }, ) hpaAnalyzer := HpaAnalyzer{} config := common.Analyzer{ Client: &kubernetes.Client{ Client: clientset, }, Context: context.Background(), Namespace: "default", LabelSelector: "app=hpa", } analysisResults, err := hpaAnalyzer.Analyze(config) if err != nil { t.Error(err) } assert.Equal(t, len(analysisResults), 1) } func TestHPAAnalyzerStatusFieldAbleToScale(t *testing.T) { clientset := fake.NewSimpleClientset( &autoscalingv2.HorizontalPodAutoscaler{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "default", Annotations: map[string]string{}, }, Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ Kind: "Deployment", Name: "example", }, }, Status: autoscalingv2.HorizontalPodAutoscalerStatus{ Conditions: []autoscalingv2.HorizontalPodAutoscalerCondition{ { Type: "AbleToScale", Status: "False", Message: "test reason", }, }, }, }) hpaAnalyzer := HpaAnalyzer{} config := common.Analyzer{ Client: &kubernetes.Client{ Client: clientset, }, Context: context.Background(), Namespace: "default", } analysisResults, err := hpaAnalyzer.Analyze(config) if err != nil { t.Error(err) } assert.Equal(t, len(analysisResults), 1) } func TestHPAAnalyzerStatusFieldScalingActive(t *testing.T) { clientset := fake.NewSimpleClientset( &autoscalingv2.HorizontalPodAutoscaler{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "default", Annotations: map[string]string{}, }, Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ Kind: "Deployment", Name: "example", }, }, Status: autoscalingv2.HorizontalPodAutoscalerStatus{ Conditions: []autoscalingv2.HorizontalPodAutoscalerCondition{ { Type: autoscalingv2.ScalingActive, Status: "False", Message: "test reason", }, }, }, }) hpaAnalyzer := HpaAnalyzer{} config := common.Analyzer{ Client: &kubernetes.Client{ Client: clientset, }, Context: context.Background(), Namespace: "default", } analysisResults, err := hpaAnalyzer.Analyze(config) if err != nil { t.Error(err) } assert.Equal(t, len(analysisResults), 1) } func TestHPAAnalyzerStatusFieldScalingLimited(t *testing.T) { clientset := fake.NewSimpleClientset( &autoscalingv2.HorizontalPodAutoscaler{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "default", Annotations: map[string]string{}, }, Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ Kind: "Deployment", Name: "example", }, }, Status: autoscalingv2.HorizontalPodAutoscalerStatus{ Conditions: []autoscalingv2.HorizontalPodAutoscalerCondition{ { Type: autoscalingv2.ScalingLimited, Status: "False", Message: "test reason", }, }, }, }) hpaAnalyzer := HpaAnalyzer{} config := common.Analyzer{ Client: &kubernetes.Client{ Client: clientset, }, Context: context.Background(), Namespace: "default", } analysisResults, err := hpaAnalyzer.Analyze(config) if err != nil { t.Error(err) } assert.Equal(t, len(analysisResults), 1) } func TestHPAAnalyzerStatusField(t *testing.T) { clientset := fake.NewSimpleClientset( &autoscalingv2.HorizontalPodAutoscaler{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "default", Annotations: map[string]string{}, }, Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ Kind: "Deployment", Name: "example", }, }, Status: autoscalingv2.HorizontalPodAutoscalerStatus{ Conditions: []autoscalingv2.HorizontalPodAutoscalerCondition{ { Type: autoscalingv2.AbleToScale, Status: "True", Message: "recommended size matches current size", }, { Type: autoscalingv2.ScalingActive, Status: "True", Message: "the HPA was able to successfully calculate a replica count", }, { Type: autoscalingv2.ScalingLimited, Status: "True", Message: "the desired replica count is less than the minimum replica count", }, }, }, }) hpaAnalyzer := HpaAnalyzer{} config := common.Analyzer{ Client: &kubernetes.Client{ Client: clientset, }, Context: context.Background(), Namespace: "default", } analysisResults, err := hpaAnalyzer.Analyze(config) if err != nil { t.Error(err) } assert.Equal(t, len(analysisResults), 1) } func TestHPAAnalyzerStatusScalingLimitedError(t *testing.T) { clientset := fake.NewSimpleClientset( &autoscalingv2.HorizontalPodAutoscaler{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "default", Annotations: map[string]string{}, }, Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ Kind: "Deployment", Name: "example", }, }, Status: autoscalingv2.HorizontalPodAutoscalerStatus{ Conditions: []autoscalingv2.HorizontalPodAutoscalerCondition{ { Type: autoscalingv2.AbleToScale, Status: "True", Message: "recommended size matches current size", }, { Type: autoscalingv2.ScalingActive, Status: "True", Message: "the HPA was able to successfully calculate a replica count", }, { Type: autoscalingv2.ScalingLimited, Status: "True", Message: "the desired replica count is less than the minimum replica count", }, }, }, }, &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "default", Annotations: map[string]string{}, }, Spec: appsv1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "example", Image: "nginx", }, }, }, }, }, }, ) hpaAnalyzer := HpaAnalyzer{} config := common.Analyzer{ Client: &kubernetes.Client{ Client: clientset, }, Context: context.Background(), Namespace: "default", } analysisResults, err := hpaAnalyzer.Analyze(config) if err != nil { t.Error(err) } var errorFound bool want := "the desired replica count is less than the minimum replica count" for _, analysis := range analysisResults { for _, got := range analysis.Error { if want == got.Text { errorFound = true } } if errorFound { break } } if !errorFound { t.Errorf("Expected message, <%v> , not found in HorizontalPodAutoscaler's analysis results", want) } } ================================================ FILE: pkg/analyzer/httproute.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package analyzer import ( "fmt" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/util" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" ctrl "sigs.k8s.io/controller-runtime/pkg/client" gtwapi "sigs.k8s.io/gateway-api/apis/v1" ) type HTTPRouteAnalyzer struct{} // Gateway analyser will analyse all different Kinds and search for missing object dependencies func (HTTPRouteAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) { kind := "HTTPRoute" AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{ "analyzer_name": kind, }) routeList := >wapi.HTTPRouteList{} gtw := >wapi.Gateway{} service := &corev1.Service{} client := a.Client.CtrlClient err := gtwapi.AddToScheme(client.Scheme()) if err != nil { return nil, err } labelSelector := util.LabelStrToSelector(a.LabelSelector) if err := client.List(a.Context, routeList, &ctrl.ListOptions{LabelSelector: labelSelector}); err != nil { return nil, err } var preAnalysis = map[string]common.PreAnalysis{} // Find all unhealthy gateway Classes for _, route := range routeList.Items { var failures []common.Failure // Check if Gateways exists in the same or designated namespace // TODO: when meshes and ClusterIp options are adopted we can add more checks // e.g Service Port matching for _, gtwref := range route.Spec.ParentRefs { namespace := route.Namespace if gtwref.Namespace != nil { namespace = string(*gtwref.Namespace) } err := client.Get(a.Context, ctrl.ObjectKey{Namespace: namespace, Name: string(gtwref.Name)}, gtw, &ctrl.GetOptions{}) if errors.IsNotFound(err) { failures = append(failures, common.Failure{ Text: fmt.Sprintf( "HTTPRoute uses the Gateway '%s/%s' which does not exist in the same namespace.", namespace, gtwref.Name, ), Sensitive: []common.Sensitive{ { Unmasked: gtw.Namespace, Masked: util.MaskString(gtw.Namespace), }, { Unmasked: gtw.Name, Masked: util.MaskString(gtw.Name), }, }, }) } else { // Check if the aforementioned Gateway allows the HTTPRoutes from the route's namespace for _, listener := range gtw.Spec.Listeners { if listener.AllowedRoutes.Namespaces != nil { switch allow := listener.AllowedRoutes.Namespaces.From; { case *allow == gtwapi.NamespacesFromSame: // check if Gateway is in the same namespace if route.Namespace != gtw.Namespace { failures = append(failures, common.Failure{ Text: fmt.Sprintf("HTTPRoute '%s/%s' is deployed in a different namespace from Gateway '%s/%s' which only allows HTTPRoutes from its namespace.", route.Namespace, route.Name, gtw.Namespace, gtw.Name, ), Sensitive: []common.Sensitive{ { Unmasked: route.Namespace, Masked: util.MaskString(route.Namespace), }, { Unmasked: route.Name, Masked: util.MaskString(route.Name), }, { Unmasked: gtw.Namespace, Masked: util.MaskString(gtw.Namespace), }, { Unmasked: gtw.Name, Masked: util.MaskString(gtw.Name), }, }, }) } case *allow == gtwapi.NamespacesFromSelector: // check if our route include the same selector Label if !util.LabelsIncludeAny(listener.AllowedRoutes.Namespaces.Selector.MatchLabels, route.Labels) { failures = append(failures, common.Failure{ Text: fmt.Sprintf( "HTTPRoute '%s/%s' can't be attached on Gateway '%s/%s', selector labels do not match HTTProute's labels.", route.Namespace, route.Name, gtw.Namespace, gtw.Name, ), Sensitive: []common.Sensitive{ { Unmasked: route.Namespace, Masked: util.MaskString(route.Namespace), }, { Unmasked: route.Name, Masked: util.MaskString(route.Name), }, { Unmasked: gtw.Namespace, Masked: util.MaskString(gtw.Namespace), }, { Unmasked: gtw.Name, Masked: util.MaskString(gtw.Name), }, }, }) } } } } } } // Check if the Backends are valid services and ports are matching with services Ports for _, rule := range route.Spec.Rules { for _, backend := range rule.BackendRefs { err := client.Get(a.Context, ctrl.ObjectKey{Namespace: route.Namespace, Name: string(backend.Name)}, service, &ctrl.GetOptions{}) if errors.IsNotFound(err) { failures = append(failures, common.Failure{ Text: fmt.Sprintf( "HTTPRoute uses the Service '%s/%s' which does not exist.", route.Namespace, backend.Name, ), Sensitive: []common.Sensitive{ { Unmasked: service.Namespace, Masked: util.MaskString(service.Namespace), }, { Unmasked: service.Name, Masked: util.MaskString(service.Name), }, }, }) } else { portMatch := false for _, svcPort := range service.Spec.Ports { if int32(*backend.Port) == svcPort.Port { portMatch = true } } if !portMatch { failures = append(failures, common.Failure{ Text: fmt.Sprintf( "HTTPRoute's backend service '%s' is using port '%d' but the corresponding K8s service '%s/%s' isn't configured with the same port.", backend.Name, int32(*backend.Port), service.Namespace, service.Name, ), Sensitive: []common.Sensitive{ { Unmasked: string(backend.Name), Masked: util.MaskString(string(backend.Name)), }, { Unmasked: service.Name, Masked: util.MaskString(service.Name), }, { Unmasked: service.Namespace, Masked: service.Namespace, }, }, }) } } } } if len(failures) > 0 { preAnalysis[fmt.Sprintf("%s/%s", route.Namespace, route.Name)] = common.PreAnalysis{ HTTPRoute: route, FailureDetails: failures, } AnalyzerErrorsMetric.WithLabelValues(kind, route.Name, route.Namespace).Set(float64(len(failures))) } } for key, value := range preAnalysis { var currentAnalysis = common.Result{ Kind: kind, Name: key, Error: value.FailureDetails, } a.Results = append(a.Results, currentAnalysis) } return a.Results, nil } ================================================ FILE: pkg/analyzer/httproute_test.go ================================================ package analyzer import ( "context" "testing" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" corev1 "k8s.io/api/core/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/client-go/kubernetes/scheme" fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" gtwapi "sigs.k8s.io/gateway-api/apis/v1" ) func BuildRouteGateway(namespace, name, fromNamespaceref string) gtwapi.Gateway { routeNamespace := >wapi.RouteNamespaces{} switch fromNamespaceref { case "Same": fromSame := gtwapi.NamespacesFromSame routeNamespace.From = &fromSame case "Selector": fromSelector := gtwapi.NamespacesFromSelector routeNamespace.From = &fromSelector routeNamespace.Selector = &metav1.LabelSelector{} routeNamespace.Selector.MatchLabels = map[string]string{"foo": "bar"} default: fromAll := gtwapi.NamespacesFromAll routeNamespace.From = &fromAll } Gateway := gtwapi.Gateway{} Gateway.Name = name Gateway.Namespace = namespace Gateway.Spec.GatewayClassName = "fooclassName" Gateway.Spec.Listeners = []gtwapi.Listener{ { Name: "proxy", Port: 80, Protocol: gtwapi.HTTPProtocolType, AllowedRoutes: >wapi.AllowedRoutes{ Namespaces: routeNamespace, }, }, } Condition := metav1.Condition{ Type: "Accepted", Status: "True", Message: "An expected message", Reason: "Test", } Gateway.Status.Conditions = []metav1.Condition{Condition} return Gateway } func BuildHTTPRoute(backendName, gtwName gtwapi.ObjectName, gtwNamespace gtwapi.Namespace, svcPort *gtwapi.PortNumber, namespace string) gtwapi.HTTPRoute { HTTPRoute := gtwapi.HTTPRoute{} HTTPRoute.Name = "foohttproute" HTTPRoute.Namespace = namespace HTTPRoute.Spec.ParentRefs = []gtwapi.ParentReference{ { Name: gtwName, Namespace: >wNamespace, }, } HTTPRoute.Spec.Rules = []gtwapi.HTTPRouteRule{ { BackendRefs: []gtwapi.HTTPBackendRef{ { BackendRef: gtwapi.BackendRef{ BackendObjectReference: gtwapi.BackendObjectReference{ Name: backendName, Port: svcPort, }, }, }, }, }, } return HTTPRoute } /* Testing different cases 1. Gateway doesn't exist or at least doesn't exist in the same namespace 2. Gateway exists in different namespace, is configured in httproute's spec and Gateway's configuration is allowing only from its same namespace 3. Gateway exists in the same namespace but has selectors different from route's labels 4. BackendRef is pointing to a non existent Service 5. BackendRef's port and Service Port are different */ func TestGWMissiningHTTRouteAnalyzer(t *testing.T) { backendName := gtwapi.ObjectName("foobackend") gtwName := gtwapi.ObjectName("non-existent") gtwNamespace := gtwapi.Namespace("non-existent") svcPort := gtwapi.PortNumber(1027) httpRouteNamespace := "default" HTTPRoute := BuildHTTPRoute(backendName, gtwName, gtwNamespace, &svcPort, httpRouteNamespace) // Create a Gateway Analyzer instance with the fake client scheme := scheme.Scheme err := gtwapi.Install(scheme) if err != nil { t.Error(err) } err = apiextensionsv1.AddToScheme(scheme) if err != nil { t.Error(err) } objects := []runtime.Object{ &HTTPRoute, } fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(objects...).Build() analyzerInstance := HTTPRouteAnalyzer{} config := common.Analyzer{ Client: &kubernetes.Client{ CtrlClient: fakeClient, }, Context: context.Background(), Namespace: "default", } analysisResults, err := analyzerInstance.Analyze(config) if err != nil { t.Error(err) } var errorFound bool want := "HTTPRoute uses the Gateway 'non-existent/non-existent' which does not exist in the same namespace." for _, analysis := range analysisResults { for _, got := range analysis.Error { if want == got.Text { errorFound = true } } if errorFound { break } } if !errorFound { t.Errorf("Expected message, <%s> , not found in HTTPRoute's analysis results", want) } } func TestGWConfigSameHTTRouteAnalyzer(t *testing.T) { backendName := gtwapi.ObjectName("foobackend") gtwName := gtwapi.ObjectName("gatewayname") gtwNamespace := gtwapi.Namespace("differentnamespace") svcPort := gtwapi.PortNumber(1027) httpRouteNamespace := "default" HTTPRoute := BuildHTTPRoute(backendName, gtwName, gtwNamespace, &svcPort, httpRouteNamespace) Gateway := BuildRouteGateway("differentnamespace", "gatewayname", "Same") // Create a Gateway Analyzer instance with the fake client scheme := scheme.Scheme err := gtwapi.Install(scheme) if err != nil { t.Error(err) } err = apiextensionsv1.AddToScheme(scheme) if err != nil { t.Error(err) } objects := []runtime.Object{ &HTTPRoute, &Gateway, } fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(objects...).Build() analyzerInstance := HTTPRouteAnalyzer{} config := common.Analyzer{ Client: &kubernetes.Client{ CtrlClient: fakeClient, }, Context: context.Background(), Namespace: "default", } analysisResults, err := analyzerInstance.Analyze(config) if err != nil { t.Error(err) } var errorFound bool want := "HTTPRoute 'default/foohttproute' is deployed in a different namespace from Gateway 'differentnamespace/gatewayname' which only allows HTTPRoutes from its namespace." for _, analysis := range analysisResults { for _, got := range analysis.Error { if want == got.Text { errorFound = true } } if errorFound { break } } if !errorFound { t.Errorf("Expected message, <%s> , not found in HTTPRoute's analysis results", want) } } func TestGWConfigSelectorHTTRouteAnalyzer(t *testing.T) { backendName := gtwapi.ObjectName("foobackend") gtwName := gtwapi.ObjectName("gatewayname") gtwNamespace := gtwapi.Namespace("default") svcPort := gtwapi.PortNumber(1027) httpRouteNamespace := "default" HTTPRoute := BuildHTTPRoute(backendName, gtwName, gtwNamespace, &svcPort, httpRouteNamespace) Gateway := BuildRouteGateway("default", "gatewayname", "Selector") // Create a Gateway Analyzer instance with the fake client scheme := scheme.Scheme err := gtwapi.Install(scheme) if err != nil { t.Error(err) } err = apiextensionsv1.AddToScheme(scheme) if err != nil { t.Error(err) } objects := []runtime.Object{ &HTTPRoute, &Gateway, } fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(objects...).Build() analyzerInstance := HTTPRouteAnalyzer{} config := common.Analyzer{ Client: &kubernetes.Client{ CtrlClient: fakeClient, }, Context: context.Background(), Namespace: "default", } analysisResults, err := analyzerInstance.Analyze(config) if err != nil { t.Error(err) } var errorFound bool want := "HTTPRoute 'default/foohttproute' can't be attached on Gateway 'default/gatewayname', selector labels do not match HTTProute's labels." for _, analysis := range analysisResults { for _, got := range analysis.Error { if want == got.Text { errorFound = true } } if errorFound { break } } if !errorFound { t.Errorf("Expected message, <%s> , not found in HTTPRoute's analysis results", want) } } func TestSvcMissingHTTRouteAnalyzer(t *testing.T) { backendName := gtwapi.ObjectName("foobackend") gtwName := gtwapi.ObjectName("gatewayname") gtwNamespace := gtwapi.Namespace("default") svcPort := gtwapi.PortNumber(1027) httpRouteNamespace := "default" HTTPRoute := BuildHTTPRoute(backendName, gtwName, gtwNamespace, &svcPort, httpRouteNamespace) Gateway := BuildRouteGateway("default", "gatewayname", "Same") // Create a Gateway Analyzer instance with the fake client scheme := scheme.Scheme err := gtwapi.Install(scheme) if err != nil { t.Error(err) } err = apiextensionsv1.AddToScheme(scheme) if err != nil { t.Error(err) } objects := []runtime.Object{ &HTTPRoute, &Gateway, } fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(objects...).Build() analyzerInstance := HTTPRouteAnalyzer{} config := common.Analyzer{ Client: &kubernetes.Client{ CtrlClient: fakeClient, }, Context: context.Background(), Namespace: "default", } analysisResults, err := analyzerInstance.Analyze(config) if err != nil { t.Error(err) } var errorFound bool want := "HTTPRoute uses the Service 'default/foobackend' which does not exist." for _, analysis := range analysisResults { for _, got := range analysis.Error { if want == got.Text { errorFound = true } } if errorFound { break } } if !errorFound { t.Errorf("Expected message, <%s> , not found in HTTPRoute's analysis results", want) } } func TestSvcDifferentPortHTTRouteAnalyzer(t *testing.T) { //Add a Service Object Service := corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "foobackend", Namespace: "default", }, Spec: corev1.ServiceSpec{ Selector: map[string]string{ "app": "example-app", }, Ports: []corev1.ServicePort{ { Name: "http", Protocol: "TCP", Port: 80, TargetPort: intstr.FromInt(8080), }, }, Type: corev1.ServiceTypeClusterIP, }, } backendName := gtwapi.ObjectName("foobackend") gtwName := gtwapi.ObjectName("gatewayname") gtwNamespace := gtwapi.Namespace("default") // different port svcPort := gtwapi.PortNumber(1027) httpRouteNamespace := "default" HTTPRoute := BuildHTTPRoute(backendName, gtwName, gtwNamespace, &svcPort, httpRouteNamespace) Gateway := BuildRouteGateway("default", "gatewayname", "Same") // Create a Gateway Analyzer instance with the fake client scheme := scheme.Scheme err := gtwapi.Install(scheme) if err != nil { t.Error(err) } err = apiextensionsv1.AddToScheme(scheme) if err != nil { t.Error(err) } objects := []runtime.Object{ &HTTPRoute, &Gateway, &Service, } fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(objects...).Build() analyzerInstance := HTTPRouteAnalyzer{} config := common.Analyzer{ Client: &kubernetes.Client{ CtrlClient: fakeClient, }, Context: context.Background(), Namespace: "default", } analysisResults, err := analyzerInstance.Analyze(config) if err != nil { t.Error(err) } var errorFound bool want := "HTTPRoute's backend service 'foobackend' is using port '1027' but the corresponding K8s service 'default/foobackend' isn't configured with the same port." for _, analysis := range analysisResults { for _, got := range analysis.Error { if want == got.Text { errorFound = true } } if errorFound { break } } if !errorFound { t.Errorf("Expected message, <%s> , not found in HTTPRoute's analysis results", want) } } ================================================ FILE: pkg/analyzer/ingress.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package analyzer import ( "fmt" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" "github.com/k8sgpt-ai/k8sgpt/pkg/util" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" ) type IngressAnalyzer struct{} func (IngressAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) { kind := "Ingress" apiDoc := kubernetes.K8sApiReference{ Kind: kind, ApiVersion: schema.GroupVersion{ Group: "networking", Version: "v1", }, OpenapiSchema: a.OpenapiSchema, } AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{ "analyzer_name": kind, }) list, err := a.Client.GetClient().NetworkingV1().Ingresses(a.Namespace).List(a.Context, metav1.ListOptions{LabelSelector: a.LabelSelector}) if err != nil { return nil, err } var preAnalysis = map[string]common.PreAnalysis{} for _, ing := range list.Items { var failures []common.Failure // get ingressClassName ingressClassName := ing.Spec.IngressClassName if ingressClassName == nil { ingClassValue := ing.Annotations["kubernetes.io/ingress.class"] if ingClassValue == "" { doc := apiDoc.GetApiDocV2("spec.ingressClassName") failures = append(failures, common.Failure{ Text: fmt.Sprintf("Ingress %s/%s does not specify an Ingress class.", ing.Namespace, ing.Name), KubernetesDoc: doc, Sensitive: []common.Sensitive{ { Unmasked: ing.Namespace, Masked: util.MaskString(ing.Namespace), }, { Unmasked: ing.Name, Masked: util.MaskString(ing.Name), }, }, }) } else { ingressClassName = &ingClassValue } } // check if ingressclass exist if ingressClassName != nil { _, err := a.Client.GetClient().NetworkingV1().IngressClasses().Get(a.Context, *ingressClassName, metav1.GetOptions{}) if err != nil { doc := apiDoc.GetApiDocV2("spec.ingressClassName") failures = append(failures, common.Failure{ Text: fmt.Sprintf("Ingress uses the ingress class %s which does not exist.", *ingressClassName), KubernetesDoc: doc, Sensitive: []common.Sensitive{ { Unmasked: *ingressClassName, Masked: util.MaskString(*ingressClassName), }, }, }) } } // loop over rules for _, rule := range ing.Spec.Rules { // loop over HTTP paths if rule.HTTP != nil { for _, path := range rule.HTTP.Paths { _, err := a.Client.GetClient().CoreV1().Services(ing.Namespace).Get(a.Context, path.Backend.Service.Name, metav1.GetOptions{}) if err != nil { doc := apiDoc.GetApiDocV2("spec.rules.http.paths.backend.service") failures = append(failures, common.Failure{ Text: fmt.Sprintf("Ingress uses the service %s/%s which does not exist.", ing.Namespace, path.Backend.Service.Name), KubernetesDoc: doc, Sensitive: []common.Sensitive{ { Unmasked: ing.Namespace, Masked: util.MaskString(ing.Namespace), }, { Unmasked: path.Backend.Service.Name, Masked: util.MaskString(path.Backend.Service.Name), }, }, }) } } } } for _, tls := range ing.Spec.TLS { _, err := a.Client.GetClient().CoreV1().Secrets(ing.Namespace).Get(a.Context, tls.SecretName, metav1.GetOptions{}) if err != nil { doc := apiDoc.GetApiDocV2("spec.tls.secretName") failures = append(failures, common.Failure{ Text: fmt.Sprintf("Ingress uses the secret %s/%s as a TLS certificate which does not exist.", ing.Namespace, tls.SecretName), KubernetesDoc: doc, Sensitive: []common.Sensitive{ { Unmasked: ing.Namespace, Masked: util.MaskString(ing.Namespace), }, { Unmasked: tls.SecretName, Masked: util.MaskString(tls.SecretName), }, }, }) } } if len(failures) > 0 { preAnalysis[fmt.Sprintf("%s/%s", ing.Namespace, ing.Name)] = common.PreAnalysis{ Ingress: ing, FailureDetails: failures, } AnalyzerErrorsMetric.WithLabelValues(kind, ing.Name, ing.Namespace).Set(float64(len(failures))) } } for key, value := range preAnalysis { var currentAnalysis = common.Result{ Kind: kind, Name: key, Error: value.FailureDetails, } parent, found := util.GetParent(a.Client, value.Ingress.ObjectMeta) if found { currentAnalysis.ParentObject = parent } a.Results = append(a.Results, currentAnalysis) } return a.Results, nil } ================================================ FILE: pkg/analyzer/ingress_test.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package analyzer import ( "context" "testing" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" networkingv1 "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/fake" ) func TestIngressAnalyzer(t *testing.T) { // Create test cases testCases := []struct { name string ingress *networkingv1.Ingress expectedIssues []string }{ { name: "Non-existent backend service", ingress: &networkingv1.Ingress{ ObjectMeta: metav1.ObjectMeta{ Name: "test-ingress", Namespace: "default", }, Spec: networkingv1.IngressSpec{ Rules: []networkingv1.IngressRule{ { Host: "example.com", IngressRuleValue: networkingv1.IngressRuleValue{ HTTP: &networkingv1.HTTPIngressRuleValue{ Paths: []networkingv1.HTTPIngressPath{ { Path: "/", Backend: networkingv1.IngressBackend{ Service: &networkingv1.IngressServiceBackend{ Name: "non-existent-service", Port: networkingv1.ServiceBackendPort{ Number: 80, }, }, }, }, }, }, }, }, }, }, }, expectedIssues: []string{ "Ingress default/test-ingress does not specify an Ingress class.", "Ingress uses the service default/non-existent-service which does not exist.", }, }, { name: "Non-existent TLS secret", ingress: &networkingv1.Ingress{ ObjectMeta: metav1.ObjectMeta{ Name: "test-ingress-tls", Namespace: "default", }, Spec: networkingv1.IngressSpec{ TLS: []networkingv1.IngressTLS{ { Hosts: []string{"example.com"}, SecretName: "non-existent-secret", }, }, Rules: []networkingv1.IngressRule{ { Host: "example.com", IngressRuleValue: networkingv1.IngressRuleValue{ HTTP: &networkingv1.HTTPIngressRuleValue{ Paths: []networkingv1.HTTPIngressPath{ { Path: "/", Backend: networkingv1.IngressBackend{ Service: &networkingv1.IngressServiceBackend{ Name: "test-service", Port: networkingv1.ServiceBackendPort{ Number: 80, }, }, }, }, }, }, }, }, }, }, }, expectedIssues: []string{ "Ingress default/test-ingress-tls does not specify an Ingress class.", "Ingress uses the service default/test-service which does not exist.", "Ingress uses the secret default/non-existent-secret as a TLS certificate which does not exist.", }, }, { name: "Multiple issues", ingress: &networkingv1.Ingress{ ObjectMeta: metav1.ObjectMeta{ Name: "test-ingress-multi", Namespace: "default", }, Spec: networkingv1.IngressSpec{ TLS: []networkingv1.IngressTLS{ { Hosts: []string{"example.com"}, SecretName: "non-existent-secret", }, }, Rules: []networkingv1.IngressRule{ { Host: "example.com", IngressRuleValue: networkingv1.IngressRuleValue{ HTTP: &networkingv1.HTTPIngressRuleValue{ Paths: []networkingv1.HTTPIngressPath{ { Path: "/", Backend: networkingv1.IngressBackend{ Service: &networkingv1.IngressServiceBackend{ Name: "non-existent-service", Port: networkingv1.ServiceBackendPort{ Number: 80, }, }, }, }, }, }, }, }, }, }, }, expectedIssues: []string{ "Ingress default/test-ingress-multi does not specify an Ingress class.", "Ingress uses the service default/non-existent-service which does not exist.", "Ingress uses the secret default/non-existent-secret as a TLS certificate which does not exist.", }, }, } // Run test cases for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { // Create a new context and clientset for each test case ctx := context.Background() clientset := fake.NewSimpleClientset() // Create the ingress in the fake clientset _, err := clientset.NetworkingV1().Ingresses(tc.ingress.Namespace).Create(ctx, tc.ingress, metav1.CreateOptions{}) assert.NoError(t, err) // Create the analyzer configuration config := common.Analyzer{ Client: &kubernetes.Client{ Client: clientset, }, Context: ctx, Namespace: tc.ingress.Namespace, } // Create the analyzer and run analysis analyzer := IngressAnalyzer{} results, err := analyzer.Analyze(config) assert.NoError(t, err) // Check that we got the expected number of issues assert.Len(t, results, 1, "Expected 1 result") result := results[0] assert.Len(t, result.Error, len(tc.expectedIssues), "Expected %d issues, got %d", len(tc.expectedIssues), len(result.Error)) // Check that each expected issue is present for _, expectedIssue := range tc.expectedIssues { found := false for _, failure := range result.Error { if failure.Text == expectedIssue { found = true break } } assert.True(t, found, "Expected to find issue: %s", expectedIssue) } }) } } func TestIngressAnalyzerLabelSelector(t *testing.T) { clientSet := fake.NewSimpleClientset( &networkingv1.Ingress{ ObjectMeta: metav1.ObjectMeta{ Name: "ingress-with-label", Namespace: "default", Labels: map[string]string{ "app": "test", }, }, Spec: networkingv1.IngressSpec{ // Missing ingress class to trigger a failure }, }, &networkingv1.Ingress{ ObjectMeta: metav1.ObjectMeta{ Name: "ingress-without-label", Namespace: "default", }, Spec: networkingv1.IngressSpec{ // Missing ingress class to trigger a failure }, }, ) // Test with label selector config := common.Analyzer{ Client: &kubernetes.Client{ Client: clientSet, }, Context: context.Background(), Namespace: "default", LabelSelector: "app=test", } analyzer := IngressAnalyzer{} results, err := analyzer.Analyze(config) require.NoError(t, err) require.Equal(t, 1, len(results)) require.Equal(t, "default/ingress-with-label", results[0].Name) } // Helper functions func strPtr(s string) *string { return &s } func pathTypePtr(p networkingv1.PathType) *networkingv1.PathType { return &p } ================================================ FILE: pkg/analyzer/installplan_test.go ================================================ package analyzer import ( "context" "strings" "testing" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" dynamicfake "k8s.io/client-go/dynamic/fake" ) func TestInstallPlanAnalyzer(t *testing.T) { ok := &unstructured.Unstructured{ Object: map[string]any{ "apiVersion": "operators.coreos.com/v1alpha1", "kind": "InstallPlan", "metadata": map[string]any{ "name": "ip-ok", "namespace": "ns1", }, "status": map[string]any{"phase": "Complete"}, }, } bad := &unstructured.Unstructured{ Object: map[string]any{ "apiVersion": "operators.coreos.com/v1alpha1", "kind": "InstallPlan", "metadata": map[string]any{ "name": "ip-bad", "namespace": "ns1", }, "status": map[string]any{ "phase": "Failed", "conditions": []interface{}{ map[string]any{ "reason": "ExecutionError", "message": "something went wrong", }, }, }, }, } listKinds := map[schema.GroupVersionResource]string{ {Group: "operators.coreos.com", Version: "v1alpha1", Resource: "installplans"}: "InstallPlanList", } scheme := runtime.NewScheme() dc := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(scheme, listKinds, ok, bad) a := common.Analyzer{ Context: context.TODO(), Client: &kubernetes.Client{DynamicClient: dc}, } res, err := (InstallPlanAnalyzer{}).Analyze(a) if err != nil { t.Fatalf("Analyze error: %v", err) } if len(res) != 1 { t.Fatalf("expected 1 result, got %d", len(res)) } if res[0].Kind != "InstallPlan" || !strings.Contains(res[0].Name, "ns1/ip-bad") { t.Fatalf("unexpected result: %#v", res[0]) } if len(res[0].Error) == 0 || !strings.Contains(res[0].Error[0].Text, "ExecutionError") { t.Fatalf("expected 'ExecutionError' in failure, got %#v", res[0].Error) } } ================================================ FILE: pkg/analyzer/instalplan.go ================================================ package analyzer import ( "fmt" "github.com/k8sgpt-ai/k8sgpt/pkg/common" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" ) type InstallPlanAnalyzer struct{} var ipGVR = schema.GroupVersionResource{ Group: "operators.coreos.com", Version: "v1alpha1", Resource: "installplans", } func (InstallPlanAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) { kind := "InstallPlan" if a.Client.GetDynamicClient() == nil { return nil, fmt.Errorf("dynamic client is nil in %s analyzer", kind) } list, err := a.Client.GetDynamicClient(). Resource(ipGVR).Namespace(metav1.NamespaceAll). List(a.Context, metav1.ListOptions{}) if err != nil { return nil, err } var results []common.Result for _, item := range list.Items { ns, name := item.GetNamespace(), item.GetName() phase, _, _ := unstructured.NestedString(item.Object, "status", "phase") var failures []common.Failure if phase != "" && phase != "Complete" { reason := firstCondStr(&item, "reason") msg := firstCondStr(&item, "message") switch { case reason != "" && msg != "": failures = append(failures, common.Failure{Text: fmt.Sprintf("phase=%q: %s: %s", phase, reason, msg)}) case reason != "" || msg != "": failures = append(failures, common.Failure{Text: fmt.Sprintf("phase=%q: %s%s", phase, reason, msg)}) default: failures = append(failures, common.Failure{Text: fmt.Sprintf("phase=%q (approval/manual? check status.conditions)", phase)}) } } if len(failures) > 0 { results = append(results, common.Result{ Kind: kind, Name: ns + "/" + name, Error: failures, }) } } return results, nil } func firstCondStr(u *unstructured.Unstructured, field string) string { conds, _, _ := unstructured.NestedSlice(u.Object, "status", "conditions") if len(conds) == 0 { return "" } m, _ := conds[0].(map[string]any) if m == nil { return "" } v, _ := m[field].(string) return v } ================================================ FILE: pkg/analyzer/job.go ================================================ /* Copyright 2025 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package analyzer import ( "fmt" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" "github.com/k8sgpt-ai/k8sgpt/pkg/util" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" ) type JobAnalyzer struct{} func (analyzer JobAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) { kind := "Job" apiDoc := kubernetes.K8sApiReference{ Kind: kind, ApiVersion: schema.GroupVersion{ Group: "batch", Version: "v1", }, OpenapiSchema: a.OpenapiSchema, } AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{ "analyzer_name": kind, }) JobList, err := a.Client.GetClient().BatchV1().Jobs(a.Namespace).List(a.Context, v1.ListOptions{LabelSelector: a.LabelSelector}) if err != nil { return nil, err } var preAnalysis = map[string]common.PreAnalysis{} for _, Job := range JobList.Items { var failures []common.Failure if Job.Spec.Suspend != nil && *Job.Spec.Suspend { doc := apiDoc.GetApiDocV2("spec.suspend") failures = append(failures, common.Failure{ Text: fmt.Sprintf("Job %s is suspended", Job.Name), KubernetesDoc: doc, Sensitive: []common.Sensitive{ { Unmasked: Job.Namespace, Masked: util.MaskString(Job.Namespace), }, { Unmasked: Job.Name, Masked: util.MaskString(Job.Name), }, }, }) } if Job.Status.Failed > 0 { doc := apiDoc.GetApiDocV2("status.failed") failures = append(failures, common.Failure{ Text: fmt.Sprintf("Job %s has failed", Job.Name), KubernetesDoc: doc, Sensitive: []common.Sensitive{ { Unmasked: Job.Namespace, Masked: util.MaskString(Job.Namespace), }, { Unmasked: Job.Name, Masked: util.MaskString(Job.Name), }, }, }) } if len(failures) > 0 { preAnalysis[fmt.Sprintf("%s/%s", Job.Namespace, Job.Name)] = common.PreAnalysis{ FailureDetails: failures, } AnalyzerErrorsMetric.WithLabelValues(kind, Job.Name, Job.Namespace).Set(float64(len(failures))) } } for key, value := range preAnalysis { currentAnalysis := common.Result{ Kind: kind, Name: key, Error: value.FailureDetails, } a.Results = append(a.Results, currentAnalysis) } return a.Results, nil } ================================================ FILE: pkg/analyzer/job_test.go ================================================ /* Copyright 2025 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package analyzer import ( "context" "sort" "testing" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" "github.com/stretchr/testify/require" batchv1 "k8s.io/api/batch/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/fake" ) func TestJobAnalyzer(t *testing.T) { tests := []struct { name string config common.Analyzer expectations []struct { name string failuresCount int } }{ { name: "Suspended Job", config: common.Analyzer{ Client: &kubernetes.Client{ Client: fake.NewSimpleClientset( &batchv1.Job{ ObjectMeta: metav1.ObjectMeta{ Name: "suspended-job", Namespace: "default", }, Spec: batchv1.JobSpec{ Suspend: boolPtr(true), }, }, ), }, Context: context.Background(), Namespace: "default", }, expectations: []struct { name string failuresCount int }{ { name: "default/suspended-job", failuresCount: 1, // One failure for being suspended }, }, }, { name: "Failed Job", config: common.Analyzer{ Client: &kubernetes.Client{ Client: fake.NewSimpleClientset( &batchv1.Job{ ObjectMeta: metav1.ObjectMeta{ Name: "failed-job", Namespace: "default", }, Spec: batchv1.JobSpec{}, Status: batchv1.JobStatus{ Failed: 1, }, }, ), }, Context: context.Background(), Namespace: "default", }, expectations: []struct { name string failuresCount int }{ { name: "default/failed-job", failuresCount: 1, // One failure for failed job }, }, }, { name: "Valid Job", config: common.Analyzer{ Client: &kubernetes.Client{ Client: fake.NewSimpleClientset( &batchv1.Job{ ObjectMeta: metav1.ObjectMeta{ Name: "valid-job", Namespace: "default", }, Spec: batchv1.JobSpec{}, }, ), }, Context: context.Background(), Namespace: "default", }, expectations: []struct { name string failuresCount int }{ // No expectations for valid job }, }, { name: "Multiple issues", config: common.Analyzer{ Client: &kubernetes.Client{ Client: fake.NewSimpleClientset( &batchv1.Job{ ObjectMeta: metav1.ObjectMeta{ Name: "multiple-issues", Namespace: "default", }, Spec: batchv1.JobSpec{ Suspend: boolPtr(true), }, Status: batchv1.JobStatus{ Failed: 1, }, }, ), }, Context: context.Background(), Namespace: "default", }, expectations: []struct { name string failuresCount int }{ { name: "default/multiple-issues", failuresCount: 2, // Two failures: suspended and failed job }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { analyzer := JobAnalyzer{} results, err := analyzer.Analyze(tt.config) require.NoError(t, err) require.Len(t, results, len(tt.expectations)) // Sort results by name for consistent comparison sort.Slice(results, func(i, j int) bool { return results[i].Name < results[j].Name }) for i, expectation := range tt.expectations { require.Equal(t, expectation.name, results[i].Name) require.Len(t, results[i].Error, expectation.failuresCount) } }) } } func TestJobAnalyzerLabelSelector(t *testing.T) { clientSet := fake.NewSimpleClientset( &batchv1.Job{ ObjectMeta: metav1.ObjectMeta{ Name: "job-with-label", Namespace: "default", Labels: map[string]string{ "app": "test", }, }, Spec: batchv1.JobSpec{}, Status: batchv1.JobStatus{ Failed: 1, }, }, &batchv1.Job{ ObjectMeta: metav1.ObjectMeta{ Name: "job-without-label", Namespace: "default", }, Spec: batchv1.JobSpec{}, }, ) // Test with label selector config := common.Analyzer{ Client: &kubernetes.Client{ Client: clientSet, }, Context: context.Background(), Namespace: "default", LabelSelector: "app=test", } analyzer := JobAnalyzer{} results, err := analyzer.Analyze(config) require.NoError(t, err) require.Equal(t, 1, len(results)) require.Equal(t, "default/job-with-label", results[0].Name) } ================================================ FILE: pkg/analyzer/log.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package analyzer import ( "fmt" "regexp" "strings" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/util" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) var ( errorPattern = regexp.MustCompile(`(error|exception|fail)`) tailLines = int64(100) ) type LogAnalyzer struct { } func (LogAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) { kind := "Log" AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{ "analyzer_name": kind, }) // search all namespaces for pods that are not running list, err := a.Client.GetClient().CoreV1().Pods(a.Namespace).List(a.Context, metav1.ListOptions{LabelSelector: a.LabelSelector}) if err != nil { return nil, err } var preAnalysis = map[string]common.PreAnalysis{} // Iterate through each pod for _, pod := range list.Items { podName := pod.Name for _, c := range pod.Spec.Containers { var failures []common.Failure podLogOptions := v1.PodLogOptions{ TailLines: &tailLines, Container: c.Name, } podLogs, err := a.Client.Client.CoreV1().Pods(pod.Namespace).GetLogs(podName, &podLogOptions).DoRaw(a.Context) if err != nil { failures = append(failures, common.Failure{ Text: fmt.Sprintf("Error %s from Pod %s", err.Error(), pod.Name), Sensitive: []common.Sensitive{ { Unmasked: pod.Name, Masked: util.MaskString(pod.Name), }, }, }) } else { rawlogs := string(podLogs) if errorPattern.MatchString(strings.ToLower(rawlogs)) { failures = append(failures, common.Failure{ Text: printErrorLines(rawlogs, errorPattern), Sensitive: []common.Sensitive{ { Unmasked: pod.Name, Masked: util.MaskString(pod.Name), }, }, }) } } if len(failures) > 0 { preAnalysis[fmt.Sprintf("%s/%s/%s", pod.Namespace, pod.Name, c.Name)] = common.PreAnalysis{ FailureDetails: failures, Pod: pod, } AnalyzerErrorsMetric.WithLabelValues(kind, pod.Name, pod.Namespace).Set(float64(len(failures))) } } } for key, value := range preAnalysis { currentAnalysis := common.Result{ Kind: "Pod", Name: key, Error: value.FailureDetails, } parent, found := util.GetParent(a.Client, value.Pod.ObjectMeta) if found { currentAnalysis.ParentObject = parent } a.Results = append(a.Results, currentAnalysis) } return a.Results, nil } func printErrorLines(logs string, errorPattern *regexp.Regexp) string { // Split the logs into lines logLines := strings.Split(logs, "\n") // Check each line for errors and print the lines containing errors for _, line := range logLines { if errorPattern.MatchString(strings.ToLower(line)) { return line } } return "" } ================================================ FILE: pkg/analyzer/log_test.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package analyzer import ( "context" "regexp" "sort" "testing" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" "github.com/stretchr/testify/require" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/fake" ) func TestLogAnalyzer(t *testing.T) { oldPattern := errorPattern errorPattern = regexp.MustCompile(`(fake logs)`) t.Cleanup(func() { errorPattern = oldPattern }) config := common.Analyzer{ Client: &kubernetes.Client{ Client: fake.NewSimpleClientset( &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "Pod1", Namespace: "default", Labels: map[string]string{ "Name": "Pod1", "Namespace": "default", }, }, Spec: v1.PodSpec{ Containers: []v1.Container{ { Name: "test-container1", }, { Name: "test-container2", }, }, }, }, &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "Pod2", Namespace: "default", Labels: map[string]string{ "Name": "Pod1", "Namespace": "default", }, }, }, &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "Pod3", Namespace: "test-namespace", Labels: map[string]string{ "Name": "Pod1", "Namespace": "test-namespace", }, }, }, &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "Pod4", Namespace: "default", Labels: map[string]string{ "Name": "Pod4", "Namespace": "default", }, }, Spec: v1.PodSpec{ Containers: []v1.Container{ { Name: "test-container3", }, }, }, }, ), }, Context: context.Background(), Namespace: "default", } logAnalyzer := LogAnalyzer{} results, err := logAnalyzer.Analyze(config) require.NoError(t, err) sort.Slice(results, func(i, j int) bool { return results[i].Name < results[j].Name }) expectations := []string{"default/Pod1/test-container1", "default/Pod1/test-container2", "default/Pod4/test-container3"} for i, expectation := range expectations { require.Equal(t, expectation, results[i].Name) for _, failure := range results[i].Error { require.Equal(t, "fake logs", failure.Text) } } } func TestLogAnalyzerLabelSelectorFiltering(t *testing.T) { oldPattern := errorPattern errorPattern = regexp.MustCompile(`(fake logs)`) t.Cleanup(func() { errorPattern = oldPattern }) config := common.Analyzer{ Client: &kubernetes.Client{ Client: fake.NewSimpleClientset( &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "Pod1", Namespace: "default", Labels: map[string]string{ "app": "log", }, }, Spec: v1.PodSpec{ Containers: []v1.Container{ { Name: "test-container1", }, }, }, }, &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "Pod2", Namespace: "default", }, Spec: v1.PodSpec{ Containers: []v1.Container{ { Name: "test-container2", }, }, }, }, ), }, Context: context.Background(), Namespace: "default", LabelSelector: "app=log", } logAnalyzer := LogAnalyzer{} results, err := logAnalyzer.Analyze(config) require.NoError(t, err) require.Equal(t, 1, len(results)) require.Equal(t, "default/Pod1/test-container1", results[0].Name) } ================================================ FILE: pkg/analyzer/mutating_webhook.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package analyzer import ( "context" "fmt" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" "github.com/k8sgpt-ai/k8sgpt/pkg/util" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" ) type MutatingWebhookAnalyzer struct{} func (MutatingWebhookAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) { kind := "MutatingWebhookConfiguration" apiDoc := kubernetes.K8sApiReference{ Kind: kind, ApiVersion: schema.GroupVersion{ Group: "apps", Version: "v1", }, OpenapiSchema: a.OpenapiSchema, } AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{ "analyzer_name": kind, }) mutatingWebhooks, err := a.Client.GetClient().AdmissionregistrationV1().MutatingWebhookConfigurations().List(context.Background(), v1.ListOptions{LabelSelector: a.LabelSelector}) if err != nil { return nil, err } var preAnalysis = map[string]common.PreAnalysis{} for _, webhookConfig := range mutatingWebhooks.Items { for _, webhook := range webhookConfig.Webhooks { var failures []common.Failure if webhook.ClientConfig.Service == nil { continue } svc := webhook.ClientConfig.Service // Get the service service, err := a.Client.GetClient().CoreV1().Services(svc.Namespace).Get(context.Background(), svc.Name, v1.GetOptions{}) if err != nil { // If the service is not found, we can't check the pods failures = append(failures, common.Failure{ Text: fmt.Sprintf("Service %s not found as mapped to by Mutating Webhook %s", svc.Name, webhook.Name), KubernetesDoc: apiDoc.GetApiDocV2("spec.webhook.clientConfig.service"), Sensitive: []common.Sensitive{ { Unmasked: webhookConfig.Namespace, Masked: util.MaskString(webhookConfig.Namespace), }, { Unmasked: svc.Name, Masked: util.MaskString(svc.Name), }, }, }) preAnalysis[fmt.Sprintf("%s/%s", webhookConfig.Namespace, webhook.Name)] = common.PreAnalysis{ MutatingWebhook: webhookConfig, FailureDetails: failures, } AnalyzerErrorsMetric.WithLabelValues(kind, webhook.Name, webhookConfig.Namespace).Set(float64(len(failures))) continue } // When Service selectors are empty we defer to service analyser if len(service.Spec.Selector) == 0 { continue } // Get pods within service pods, err := a.Client.GetClient().CoreV1().Pods(svc.Namespace).List(context.Background(), v1.ListOptions{ LabelSelector: util.MapToString(service.Spec.Selector), }) if err != nil { return nil, err } if len(pods.Items) == 0 { failures = append(failures, common.Failure{ Text: fmt.Sprintf("No active pods found within service %s as mapped to by Mutating Webhook %s", svc.Name, webhook.Name), KubernetesDoc: apiDoc.GetApiDocV2("spec.webhook.clientConfig.service"), Sensitive: []common.Sensitive{ { Unmasked: webhookConfig.Namespace, Masked: util.MaskString(webhookConfig.Namespace), }, }, }) } for _, pod := range pods.Items { if pod.Status.Phase != "Running" { doc := apiDoc.GetApiDocV2("spec.webhook") failures = append(failures, common.Failure{ Text: fmt.Sprintf( "Mutating Webhook (%s) is pointing to an inactive receiver pod (%s)", webhook.Name, pod.Name, ), KubernetesDoc: doc, Sensitive: []common.Sensitive{ { Unmasked: webhookConfig.Namespace, Masked: util.MaskString(webhookConfig.Namespace), }, { Unmasked: webhook.Name, Masked: util.MaskString(webhook.Name), }, { Unmasked: pod.Name, Masked: util.MaskString(pod.Name), }, }, }) } } if len(failures) > 0 { preAnalysis[fmt.Sprintf("%s/%s", webhookConfig.Namespace, webhook.Name)] = common.PreAnalysis{ MutatingWebhook: webhookConfig, FailureDetails: failures, } AnalyzerErrorsMetric.WithLabelValues(kind, webhook.Name, webhookConfig.Namespace).Set(float64(len(failures))) } } } for key, value := range preAnalysis { var currentAnalysis = common.Result{ Kind: kind, Name: key, Error: value.FailureDetails, } parent, found := util.GetParent(a.Client, value.MutatingWebhook.ObjectMeta) if found { currentAnalysis.ParentObject = parent } a.Results = append(a.Results, currentAnalysis) } return a.Results, nil } ================================================ FILE: pkg/analyzer/mutating_webhook_test.go ================================================ /* Copyright 2024 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package analyzer import ( "context" "testing" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" "github.com/stretchr/testify/require" admissionregistrationv1 "k8s.io/api/admissionregistration/v1" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/fake" ) func TestMutatingWebhookAnalyzer(t *testing.T) { config := common.Analyzer{ Client: &kubernetes.Client{ Client: fake.NewSimpleClientset( &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "Pod1", Namespace: "default", Labels: map[string]string{ "pod": "Pod1", }, }, }, &v1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "test-service1", Namespace: "default", }, Spec: v1.ServiceSpec{ Selector: map[string]string{ "pod": "Pod1", }, }, }, &v1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "test-service2", Namespace: "test", }, Spec: v1.ServiceSpec{ // No such pod exists in the test namespace Selector: map[string]string{ "pod": "Pod2", }, }, }, &v1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "test-service3", Namespace: "test", }, Spec: v1.ServiceSpec{ // len(service.Spec.Selector) == 0 Selector: map[string]string{}, }, }, &admissionregistrationv1.MutatingWebhookConfiguration{ ObjectMeta: metav1.ObjectMeta{ Name: "test-mutating-webhook-config", Namespace: "test", }, Webhooks: []admissionregistrationv1.MutatingWebhook{ { // Failure: Pointing to an inactive receiver pod Name: "webhook1", ClientConfig: admissionregistrationv1.WebhookClientConfig{ Service: &admissionregistrationv1.ServiceReference{ Name: "test-service1", Namespace: "default", }, }, }, { // Failure: No active pods found in the test namespace Name: "webhook2", ClientConfig: admissionregistrationv1.WebhookClientConfig{ Service: &admissionregistrationv1.ServiceReference{ Name: "test-service2", Namespace: "test", }, }, }, { Name: "webhook3", ClientConfig: admissionregistrationv1.WebhookClientConfig{ Service: &admissionregistrationv1.ServiceReference{ Name: "test-service3", Namespace: "test", }, }, }, { // Failure: Service doesn't exist. Name: "webhook4", ClientConfig: admissionregistrationv1.WebhookClientConfig{ Service: &admissionregistrationv1.ServiceReference{ Name: "test-service4-doesn't-exist", Namespace: "test", }, }, }, { // Service is nil. Name: "webhook5", ClientConfig: admissionregistrationv1.WebhookClientConfig{}, }, }, }, ), }, Context: context.Background(), Namespace: "default", } mwAnalyzer := MutatingWebhookAnalyzer{} results, err := mwAnalyzer.Analyze(config) require.NoError(t, err) // The results should contain: webhook1, webhook2, and webhook4 resultsLen := 3 require.Equal(t, resultsLen, len(results)) } func TestMutatingWebhookAnalyzerLabelSelectorFiltering(t *testing.T) { config := common.Analyzer{ Client: &kubernetes.Client{ Client: fake.NewSimpleClientset( &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "Pod1", Namespace: "default", Labels: map[string]string{ "app": "mutating-webhook", }, }, }, &v1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "test-service1", Namespace: "default", }, Spec: v1.ServiceSpec{ Selector: map[string]string{ "app": "mutating-webhook", }, }, }, &admissionregistrationv1.MutatingWebhookConfiguration{ ObjectMeta: metav1.ObjectMeta{ Name: "test-mutating-webhook-config", Namespace: "default", Labels: map[string]string{ "app": "mutating-webhook", }, }, Webhooks: []admissionregistrationv1.MutatingWebhook{ { Name: "webhook1", ClientConfig: admissionregistrationv1.WebhookClientConfig{ Service: &admissionregistrationv1.ServiceReference{ Name: "test-service1", Namespace: "default", }, }, }, }, }, &admissionregistrationv1.MutatingWebhookConfiguration{ ObjectMeta: metav1.ObjectMeta{ Name: "test-mutating-webhook-config2", Namespace: "default", }, Webhooks: []admissionregistrationv1.MutatingWebhook{ { Name: "webhook2", ClientConfig: admissionregistrationv1.WebhookClientConfig{ Service: &admissionregistrationv1.ServiceReference{ Name: "test-service1", Namespace: "default", }, }, }, }, }, ), }, Context: context.Background(), Namespace: "default", LabelSelector: "app=mutating-webhook", } mwAnalyzer := MutatingWebhookAnalyzer{} results, err := mwAnalyzer.Analyze(config) require.NoError(t, err) require.Equal(t, 1, len(results)) require.Equal(t, "default/webhook1", results[0].Name) } ================================================ FILE: pkg/analyzer/netpol.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package analyzer import ( "fmt" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" "github.com/k8sgpt-ai/k8sgpt/pkg/util" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" ) type NetworkPolicyAnalyzer struct{} func (NetworkPolicyAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) { kind := "NetworkPolicy" apiDoc := kubernetes.K8sApiReference{ Kind: kind, ApiVersion: schema.GroupVersion{ Group: "networking", Version: "v1", }, OpenapiSchema: a.OpenapiSchema, } AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{ "analyzer_name": kind, }) // get all network policies in the namespace policies, err := a.Client.GetClient().NetworkingV1(). NetworkPolicies(a.Namespace).List(a.Context, metav1.ListOptions{LabelSelector: a.LabelSelector}) if err != nil { return nil, err } var preAnalysis = map[string]common.PreAnalysis{} for _, policy := range policies.Items { var failures []common.Failure // Check if policy allows traffic to all pods in the namespace if len(policy.Spec.PodSelector.MatchLabels) == 0 { doc := apiDoc.GetApiDocV2("spec.podSelector.matchLabels") failures = append(failures, common.Failure{ Text: fmt.Sprintf("Network policy allows traffic to all pods: %s", policy.Name), KubernetesDoc: doc, Sensitive: []common.Sensitive{ { Unmasked: policy.Name, Masked: util.MaskString(policy.Name), }, }, }) } else { // Check if policy is not applied to any pods podList, err := util.GetPodListByLabels(a.Client.GetClient(), a.Namespace, policy.Spec.PodSelector.MatchLabels) if err != nil { return nil, err } if len(podList.Items) == 0 { failures = append(failures, common.Failure{ Text: fmt.Sprintf("Network policy is not applied to any pods: %s", policy.Name), Sensitive: []common.Sensitive{ { Unmasked: policy.Name, Masked: util.MaskString(policy.Name), }, }, }) } } if len(failures) > 0 { preAnalysis[fmt.Sprintf("%s/%s", policy.Namespace, policy.Name)] = common.PreAnalysis{ FailureDetails: failures, NetworkPolicy: policy, } AnalyzerErrorsMetric.WithLabelValues(kind, policy.Name, policy.Namespace).Set(float64(len(failures))) } } for key, value := range preAnalysis { currentAnalysis := common.Result{ Kind: kind, Name: key, Error: value.FailureDetails, } a.Results = append(a.Results, currentAnalysis) } return a.Results, nil } ================================================ FILE: pkg/analyzer/netpol_test.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package analyzer import ( "context" "testing" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" "github.com/magiconair/properties/assert" v1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/fake" ) func TestNetpolNoPods(t *testing.T) { clientset := fake.NewSimpleClientset(&networkingv1.NetworkPolicy{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "default", }, Spec: networkingv1.NetworkPolicySpec{ PodSelector: metav1.LabelSelector{ MatchLabels: map[string]string{ "app": "example", }, }, Ingress: []networkingv1.NetworkPolicyIngressRule{ { From: []networkingv1.NetworkPolicyPeer{ { PodSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "app": "database", }, }, }, }, }, }, }, }) config := common.Analyzer{ Client: &kubernetes.Client{ Client: clientset, }, Context: context.Background(), Namespace: "default", } analyzer := NetworkPolicyAnalyzer{} results, err := analyzer.Analyze(config) if err != nil { t.Error(err) } assert.Equal(t, len(results), 1) assert.Equal(t, results[0].Kind, "NetworkPolicy") } func TestNetpolWithPod(t *testing.T) { clientset := fake.NewSimpleClientset(&networkingv1.NetworkPolicy{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "default", }, Spec: networkingv1.NetworkPolicySpec{ PodSelector: metav1.LabelSelector{ MatchLabels: map[string]string{ "app": "example", }, }, Ingress: []networkingv1.NetworkPolicyIngressRule{ { From: []networkingv1.NetworkPolicyPeer{ { PodSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "app": "database", }, }, }, }, }, }, }, }, &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "default", Labels: map[string]string{ "app": "example", }, }, Spec: v1.PodSpec{ Containers: []v1.Container{ { Name: "example", Image: "example", }, }, }, }) config := common.Analyzer{ Client: &kubernetes.Client{ Client: clientset, }, Context: context.Background(), Namespace: "default", } analyzer := NetworkPolicyAnalyzer{} results, err := analyzer.Analyze(config) if err != nil { t.Error(err) } assert.Equal(t, len(results), 0) } func TestNetpolNoPodsNamespaceFiltering(t *testing.T) { clientset := fake.NewSimpleClientset( &networkingv1.NetworkPolicy{ ObjectMeta: metav1.ObjectMeta{ Name: "policy-without-podselector-match-labels", Namespace: "default", }, Spec: networkingv1.NetworkPolicySpec{ PodSelector: metav1.LabelSelector{ // len(MatchLabels) == 0 should trigger a failure. // Allowing traffic to all pods. MatchLabels: map[string]string{}, }, }, }, &networkingv1.NetworkPolicy{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "default", }, Spec: networkingv1.NetworkPolicySpec{ PodSelector: metav1.LabelSelector{ MatchLabels: map[string]string{ "app": "example", }, }, Ingress: []networkingv1.NetworkPolicyIngressRule{ { From: []networkingv1.NetworkPolicyPeer{ { PodSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "app": "database", }, }, }, }, }, }, }, }, &networkingv1.NetworkPolicy{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "other-namespace", }, Spec: networkingv1.NetworkPolicySpec{ PodSelector: metav1.LabelSelector{ MatchLabels: map[string]string{ "app": "example", }, }, Ingress: []networkingv1.NetworkPolicyIngressRule{ { From: []networkingv1.NetworkPolicyPeer{ { PodSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "app": "database", }, }, }, }, }, }, }, }) config := common.Analyzer{ Client: &kubernetes.Client{ Client: clientset, }, Context: context.Background(), Namespace: "default", } analyzer := NetworkPolicyAnalyzer{} results, err := analyzer.Analyze(config) if err != nil { t.Error(err) } assert.Equal(t, len(results), 2) assert.Equal(t, results[0].Kind, "NetworkPolicy") } func TestNetpolLabelSelectorFiltering(t *testing.T) { clientset := fake.NewSimpleClientset( &networkingv1.NetworkPolicy{ ObjectMeta: metav1.ObjectMeta{ Name: "example1", Namespace: "default", Labels: map[string]string{ "app": "netpol", }, }, Spec: networkingv1.NetworkPolicySpec{ PodSelector: metav1.LabelSelector{ MatchLabels: map[string]string{ "app": "netpol", }, }, }, }, &networkingv1.NetworkPolicy{ ObjectMeta: metav1.ObjectMeta{ Name: "example2", Namespace: "default", }, }, ) config := common.Analyzer{ Client: &kubernetes.Client{ Client: clientset, }, Context: context.Background(), Namespace: "default", LabelSelector: "app=netpol", } analyzer := NetworkPolicyAnalyzer{} results, err := analyzer.Analyze(config) if err != nil { t.Error(err) } assert.Equal(t, len(results), 1) } ================================================ FILE: pkg/analyzer/node.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package analyzer import ( "fmt" v1 "k8s.io/api/core/v1" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/util" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) type NodeAnalyzer struct{} func (NodeAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) { kind := "Node" AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{ "analyzer_name": kind, }) list, err := a.Client.GetClient().CoreV1().Nodes().List(a.Context, metav1.ListOptions{LabelSelector: a.LabelSelector}) if err != nil { return nil, err } var preAnalysis = map[string]common.PreAnalysis{} for _, node := range list.Items { var failures []common.Failure for _, nodeCondition := range node.Status.Conditions { // https://kubernetes.io/docs/concepts/architecture/nodes/#condition switch nodeCondition.Type { case v1.NodeReady: if nodeCondition.Status != v1.ConditionTrue { failures = addNodeConditionFailure(failures, node.Name, nodeCondition) } // k3s `EtcdIsVoter`` should not be reported as an error case v1.NodeConditionType("EtcdIsVoter"): break default: // For other conditions: // - Report True or Unknown status as failures (for standard conditions) // - Report any unknown condition type as a failure if nodeCondition.Status == v1.ConditionTrue || nodeCondition.Status == v1.ConditionUnknown || !isKnownNodeConditionType(nodeCondition.Type) { failures = addNodeConditionFailure(failures, node.Name, nodeCondition) } } } if len(failures) > 0 { preAnalysis[node.Name] = common.PreAnalysis{ Node: node, FailureDetails: failures, } AnalyzerErrorsMetric.WithLabelValues(kind, node.Name, "").Set(float64(len(failures))) } } for key, value := range preAnalysis { var currentAnalysis = common.Result{ Kind: kind, Name: key, Error: value.FailureDetails, } parent, found := util.GetParent(a.Client, value.Node.ObjectMeta) if found { currentAnalysis.ParentObject = parent } a.Results = append(a.Results, currentAnalysis) } return a.Results, err } func addNodeConditionFailure(failures []common.Failure, nodeName string, nodeCondition v1.NodeCondition) []common.Failure { failures = append(failures, common.Failure{ Text: fmt.Sprintf("%s has condition of type %s, reason %s: %s", nodeName, nodeCondition.Type, nodeCondition.Reason, nodeCondition.Message), Sensitive: []common.Sensitive{ { Unmasked: nodeName, Masked: util.MaskString(nodeName), }, }, }) return failures } // isKnownNodeConditionType checks if the condition type is a standard Kubernetes node condition func isKnownNodeConditionType(conditionType v1.NodeConditionType) bool { switch conditionType { case v1.NodeReady, v1.NodeMemoryPressure, v1.NodeDiskPressure, v1.NodePIDPressure, v1.NodeNetworkUnavailable: return true default: return false } } ================================================ FILE: pkg/analyzer/node_test.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package analyzer import ( "context" "sort" "testing" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" "github.com/stretchr/testify/require" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/fake" ) func TestNodeAnalyzer(t *testing.T) { config := common.Analyzer{ Client: &kubernetes.Client{ Client: fake.NewSimpleClientset( &v1.Node{ // A node without Status Conditions shouldn't contribute to failures. ObjectMeta: metav1.ObjectMeta{ Name: "Node1", Namespace: "test", }, }, &v1.Node{ // Nodes are not filtered using namespace. ObjectMeta: metav1.ObjectMeta{ Name: "Node2", Namespace: "default", }, Status: v1.NodeStatus{ Conditions: []v1.NodeCondition{ { // Won't contribute to failures. Type: v1.NodeReady, Status: v1.ConditionTrue, }, { // Will contribute to failures. Type: v1.NodeReady, Status: v1.ConditionFalse, }, { // Will contribute to failures. Type: v1.NodeReady, Status: v1.ConditionUnknown, }, // Non-false statuses for the default cases contribute to failures. { Type: v1.NodeMemoryPressure, Status: v1.ConditionTrue, }, { Type: v1.NodeDiskPressure, Status: v1.ConditionTrue, }, { Type: v1.NodePIDPressure, Status: v1.ConditionTrue, }, { Type: v1.NodeNetworkUnavailable, Status: v1.ConditionTrue, }, { Type: v1.NodeMemoryPressure, Status: v1.ConditionUnknown, }, { Type: v1.NodeDiskPressure, Status: v1.ConditionUnknown, }, { Type: v1.NodePIDPressure, Status: v1.ConditionUnknown, }, { Type: v1.NodeNetworkUnavailable, Status: v1.ConditionUnknown, }, // A cloud provider may set their own condition and/or a new status // might be introduced. In such cases a failure is assumed and // the code shouldn't break, although it might be a false positive. { Type: "UnknownNodeConditionType", Status: "CompletelyUnknown", }, // These won't contribute to failures. { Type: v1.NodeMemoryPressure, Status: v1.ConditionFalse, }, { Type: v1.NodeDiskPressure, Status: v1.ConditionFalse, }, { Type: v1.NodePIDPressure, Status: v1.ConditionFalse, }, { Type: v1.NodeNetworkUnavailable, Status: v1.ConditionFalse, }, }, }, }, &v1.Node{ // A node without any failures shouldn't be present in the results. ObjectMeta: metav1.ObjectMeta{ Name: "Node3", Namespace: "test", }, Status: v1.NodeStatus{ Conditions: []v1.NodeCondition{ { // Won't contribute to failures. Type: v1.NodeReady, Status: v1.ConditionTrue, }, }, }, }, ), }, Context: context.Background(), Namespace: "test", } nAnalyzer := NodeAnalyzer{} results, err := nAnalyzer.Analyze(config) require.NoError(t, err) sort.Slice(results, func(i, j int) bool { return results[i].Name < results[j].Name }) expectations := []struct { name string failuresCount int }{ { name: "Node2", failuresCount: 11, }, } require.Equal(t, len(expectations), len(results)) for i, result := range results { require.Equal(t, expectations[i].name, result.Name) require.Equal(t, expectations[i].failuresCount, len(result.Error)) } } func TestNodeAnalyzerLabelSelectorFiltering(t *testing.T) { config := common.Analyzer{ Client: &kubernetes.Client{ Client: fake.NewSimpleClientset(&v1.Node{ ObjectMeta: metav1.ObjectMeta{ Name: "Node1", Namespace: "default", Labels: map[string]string{ "app": "node", }, }, Status: v1.NodeStatus{ Conditions: []v1.NodeCondition{ { Type: v1.NodeReady, Status: v1.ConditionFalse, }, }, }, }, &v1.Node{ ObjectMeta: metav1.ObjectMeta{ Name: "Node2", Namespace: "default", }, Status: v1.NodeStatus{ Conditions: []v1.NodeCondition{ { Type: v1.NodeReady, Status: v1.ConditionFalse, }, }, }, }, ), }, Context: context.Background(), Namespace: "default", LabelSelector: "app=node", } nAnalyzer := NodeAnalyzer{} results, err := nAnalyzer.Analyze(config) require.NoError(t, err) require.Equal(t, 1, len(results)) require.Equal(t, "Node1", results[0].Name) } ================================================ FILE: pkg/analyzer/operatorgroup.go ================================================ package analyzer import ( "fmt" "github.com/k8sgpt-ai/k8sgpt/pkg/common" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" ) type OperatorGroupAnalyzer struct{} var ogGVR = schema.GroupVersionResource{ Group: "operators.coreos.com", Version: "v1", Resource: "operatorgroups", } func (OperatorGroupAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) { kind := "OperatorGroup" if a.Client.GetDynamicClient() == nil { return nil, fmt.Errorf("dynamic client is nil in %s analyzer", kind) } list, err := a.Client.GetDynamicClient(). Resource(ogGVR).Namespace(metav1.NamespaceAll). List(a.Context, metav1.ListOptions{}) if err != nil { return nil, err } countByNS := map[string]int{} for _, it := range list.Items { countByNS[it.GetNamespace()]++ } var results []common.Result for ns, n := range countByNS { if n > 1 { results = append(results, common.Result{ Kind: kind, Name: ns, Error: []common.Failure{{Text: fmt.Sprintf("%d OperatorGroups in namespace; this can break CSV resolution", n)}}, }) } } return results, nil } ================================================ FILE: pkg/analyzer/operatorgroup_test.go ================================================ package analyzer import ( "context" "testing" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" dynamicfake "k8s.io/client-go/dynamic/fake" ) func TestOperatorGroupAnalyzer(t *testing.T) { og1 := &unstructured.Unstructured{ Object: map[string]any{ "apiVersion": "operators.coreos.com/v1", "kind": "OperatorGroup", "metadata": map[string]any{ "name": "og-1", "namespace": "ns-a", }, }, } og2 := &unstructured.Unstructured{ Object: map[string]any{ "apiVersion": "operators.coreos.com/v1", "kind": "OperatorGroup", "metadata": map[string]any{ "name": "og-2", "namespace": "ns-a", }, }, } og3 := &unstructured.Unstructured{ Object: map[string]any{ "apiVersion": "operators.coreos.com/v1", "kind": "OperatorGroup", "metadata": map[string]any{ "name": "og-3", "namespace": "ns-b", }, }, } listKinds := map[schema.GroupVersionResource]string{ {Group: "operators.coreos.com", Version: "v1", Resource: "operatorgroups"}: "OperatorGroupList", } scheme := runtime.NewScheme() dc := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(scheme, listKinds, og1, og2, og3) a := common.Analyzer{ Context: context.TODO(), Client: &kubernetes.Client{DynamicClient: dc}, } res, err := (OperatorGroupAnalyzer{}).Analyze(a) if err != nil { t.Fatalf("Analyze error: %v", err) } if len(res) != 1 { t.Fatalf("expected 1 result for ns-a overlap, got %d", len(res)) } if res[0].Kind != "OperatorGroup" || res[0].Name != "ns-a" { t.Fatalf("unexpected result: %#v", res[0]) } } ================================================ FILE: pkg/analyzer/pdb.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package analyzer import ( "fmt" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" "github.com/k8sgpt-ai/k8sgpt/pkg/util" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" ) type PdbAnalyzer struct{} func (PdbAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) { kind := "PodDisruptionBudget" apiDoc := kubernetes.K8sApiReference{ Kind: kind, ApiVersion: schema.GroupVersion{ Group: "policy", Version: "v1", }, OpenapiSchema: a.OpenapiSchema, } AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{ "analyzer_name": kind, }) list, err := a.Client.GetClient().PolicyV1().PodDisruptionBudgets(a.Namespace).List(a.Context, metav1.ListOptions{LabelSelector: a.LabelSelector}) if err != nil { return nil, err } var preAnalysis = map[string]common.PreAnalysis{} for _, pdb := range list.Items { var failures []common.Failure // Before accessing the Conditions, check if they exist or not. if len(pdb.Status.Conditions) == 0 { continue } if pdb.Status.Conditions[0].Type == "DisruptionAllowed" && pdb.Status.Conditions[0].Status == "False" { var doc string if pdb.Spec.MaxUnavailable != nil { doc = apiDoc.GetApiDocV2("spec.maxUnavailable") } if pdb.Spec.MinAvailable != nil { doc = apiDoc.GetApiDocV2("spec.minAvailable") } if pdb.Spec.Selector != nil && pdb.Spec.Selector.MatchLabels != nil { for k, v := range pdb.Spec.Selector.MatchLabels { failures = append(failures, common.Failure{ Text: fmt.Sprintf("%s, expected pdb pod label %s=%s", pdb.Status.Conditions[0].Reason, k, v), KubernetesDoc: doc, Sensitive: []common.Sensitive{ { Unmasked: k, Masked: util.MaskString(k), }, { Unmasked: v, Masked: util.MaskString(v), }, }, }) } } } if len(failures) > 0 { preAnalysis[fmt.Sprintf("%s/%s", pdb.Namespace, pdb.Name)] = common.PreAnalysis{ PodDisruptionBudget: pdb, FailureDetails: failures, } AnalyzerErrorsMetric.WithLabelValues(kind, pdb.Name, pdb.Namespace).Set(float64(len(failures))) } } for key, value := range preAnalysis { var currentAnalysis = common.Result{ Kind: kind, Name: key, Error: value.FailureDetails, } parent, found := util.GetParent(a.Client, value.PodDisruptionBudget.ObjectMeta) if found { currentAnalysis.ParentObject = parent } a.Results = append(a.Results, currentAnalysis) } return a.Results, err } ================================================ FILE: pkg/analyzer/pdb_test.go ================================================ /* Copyright 2024 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package analyzer import ( "context" "testing" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" "github.com/stretchr/testify/require" policyv1 "k8s.io/api/policy/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/client-go/kubernetes/fake" ) func TestPodDisruptionBudgetAnalyzer(t *testing.T) { config := common.Analyzer{ Client: &kubernetes.Client{ Client: fake.NewSimpleClientset( &policyv1.PodDisruptionBudget{ ObjectMeta: metav1.ObjectMeta{ Name: "PDB1", Namespace: "test", }, // Status conditions are nil. Status: policyv1.PodDisruptionBudgetStatus{ Conditions: nil, }, }, &policyv1.PodDisruptionBudget{ ObjectMeta: metav1.ObjectMeta{ Name: "PDB2", Namespace: "test", }, // Status conditions are empty. Status: policyv1.PodDisruptionBudgetStatus{ Conditions: []metav1.Condition{}, }, }, &policyv1.PodDisruptionBudget{ ObjectMeta: metav1.ObjectMeta{ Name: "PDB3", Namespace: "test", }, Status: policyv1.PodDisruptionBudgetStatus{ Conditions: []metav1.Condition{ { Type: "DisruptionAllowed", Status: "False", Reason: "test reason", }, }, }, Spec: policyv1.PodDisruptionBudgetSpec{ MaxUnavailable: &intstr.IntOrString{ Type: 0, IntVal: 17, StrVal: "17", }, MinAvailable: &intstr.IntOrString{ Type: 0, IntVal: 7, StrVal: "7", }, // MatchLabels specified. Selector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "label1": "test1", "label2": "test2", }, }, }, }, &policyv1.PodDisruptionBudget{ ObjectMeta: metav1.ObjectMeta{ Name: "PDB4", Namespace: "test", }, Status: policyv1.PodDisruptionBudgetStatus{ Conditions: []metav1.Condition{ { Type: "DisruptionAllowed", Status: "False", Reason: "test reason", }, }, }, // Match Labels Empty. Spec: policyv1.PodDisruptionBudgetSpec{ Selector: &metav1.LabelSelector{}, }, }, ), }, Context: context.Background(), Namespace: "test", } pdbAnalyzer := PdbAnalyzer{} results, err := pdbAnalyzer.Analyze(config) require.NoError(t, err) require.Equal(t, 1, len(results)) require.Equal(t, "test/PDB3", results[0].Name) } func TestPodDisruptionBudgetAnalyzerLabelSelectorFiltering(t *testing.T) { config := common.Analyzer{ Client: &kubernetes.Client{ Client: fake.NewSimpleClientset( &policyv1.PodDisruptionBudget{ ObjectMeta: metav1.ObjectMeta{ Name: "PDB1", Namespace: "default", Labels: map[string]string{ "app": "pdb", }, }, // Status conditions are nil. Status: policyv1.PodDisruptionBudgetStatus{ Conditions: []metav1.Condition{ { Type: "DisruptionAllowed", Status: "False", Reason: "test reason", }, }, }, Spec: policyv1.PodDisruptionBudgetSpec{ MaxUnavailable: &intstr.IntOrString{ Type: 0, IntVal: 17, StrVal: "17", }, MinAvailable: &intstr.IntOrString{ Type: 0, IntVal: 7, StrVal: "7", }, // MatchLabels specified. Selector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "label1": "test1", "label2": "test2", }, }, }, }, &policyv1.PodDisruptionBudget{ ObjectMeta: metav1.ObjectMeta{ Name: "PDB2", Namespace: "default", }, // Status conditions are empty. Status: policyv1.PodDisruptionBudgetStatus{ Conditions: []metav1.Condition{ { Type: "DisruptionAllowed", Status: "False", Reason: "test reason", }, }, }, Spec: policyv1.PodDisruptionBudgetSpec{ MaxUnavailable: &intstr.IntOrString{ Type: 0, IntVal: 17, StrVal: "17", }, MinAvailable: &intstr.IntOrString{ Type: 0, IntVal: 7, StrVal: "7", }, // MatchLabels specified. Selector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "label1": "test1", "label2": "test2", }, }, }, }, ), }, Context: context.Background(), Namespace: "default", LabelSelector: "app=pdb", } pdbAnalyzer := PdbAnalyzer{} results, err := pdbAnalyzer.Analyze(config) require.NoError(t, err) require.Equal(t, 1, len(results)) require.Equal(t, "default/PDB1", results[0].Name) } ================================================ FILE: pkg/analyzer/pod.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package analyzer import ( "fmt" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/util" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) type PodAnalyzer struct { } func (PodAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) { kind := "Pod" AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{ "analyzer_name": kind, }) // search all namespaces for pods that are not running list, err := a.Client.GetClient().CoreV1().Pods(a.Namespace).List(a.Context, metav1.ListOptions{ LabelSelector: a.LabelSelector, }) if err != nil { return nil, err } var preAnalysis = map[string]common.PreAnalysis{} for _, pod := range list.Items { var failures []common.Failure // Check for pending pods if pod.Status.Phase == "Pending" { // Check through container status to check for crashes for _, containerStatus := range pod.Status.Conditions { if containerStatus.Type == v1.PodScheduled && containerStatus.Reason == "Unschedulable" { if containerStatus.Message != "" { failures = append(failures, common.Failure{ Text: containerStatus.Message, Sensitive: []common.Sensitive{}, }) } } } } // Check for errors in the init containers. failures = append(failures, analyzeContainerStatusFailures(a, pod.Status.InitContainerStatuses, pod.Name, pod.Namespace, string(pod.Status.Phase))...) // Check for errors in containers. failures = append(failures, analyzeContainerStatusFailures(a, pod.Status.ContainerStatuses, pod.Name, pod.Namespace, string(pod.Status.Phase))...) if len(failures) > 0 { preAnalysis[fmt.Sprintf("%s/%s", pod.Namespace, pod.Name)] = common.PreAnalysis{ Pod: pod, FailureDetails: failures, } AnalyzerErrorsMetric.WithLabelValues(kind, pod.Name, pod.Namespace).Set(float64(len(failures))) } } for key, value := range preAnalysis { var currentAnalysis = common.Result{ Kind: kind, Name: key, Error: value.FailureDetails, } parent, found := util.GetParent(a.Client, value.Pod.ObjectMeta) if found { currentAnalysis.ParentObject = parent } a.Results = append(a.Results, currentAnalysis) } return a.Results, nil } func analyzeContainerStatusFailures(a common.Analyzer, statuses []v1.ContainerStatus, name string, namespace string, statusPhase string) []common.Failure { var failures []common.Failure // Check through container status to check for crashes or unready for _, containerStatus := range statuses { if containerStatus.State.Waiting != nil { if containerStatus.State.Waiting.Reason == "ContainerCreating" && statusPhase == "Pending" { // This represents a container that is still being created or blocked due to conditions such as OOMKilled // parse the event log and append details evt, err := util.FetchLatestEvent(a.Context, a.Client, namespace, name) if err != nil || evt == nil { continue } if isEvtErrorReason(evt.Reason) && evt.Message != "" { failures = append(failures, common.Failure{ Text: evt.Message, Sensitive: []common.Sensitive{}, }) } } else if containerStatus.State.Waiting.Reason == "CrashLoopBackOff" && containerStatus.LastTerminationState.Terminated != nil { // This represents container that is in CrashLoopBackOff state due to conditions such as OOMKilled failures = append(failures, common.Failure{ Text: fmt.Sprintf("the last termination reason is %s container=%s pod=%s", containerStatus.LastTerminationState.Terminated.Reason, containerStatus.Name, name), Sensitive: []common.Sensitive{}, }) } else if isErrorReason(containerStatus.State.Waiting.Reason) && containerStatus.State.Waiting.Message != "" { failures = append(failures, common.Failure{ Text: containerStatus.State.Waiting.Message, Sensitive: []common.Sensitive{}, }) } } else if containerStatus.State.Terminated != nil { if containerStatus.State.Terminated.ExitCode != 0 { // This represents a container that is terminated abnormally // https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#container-state-terminated exitCode := containerStatus.State.Terminated.ExitCode reason := containerStatus.State.Terminated.Reason if reason == "" { reason = "Unknown" } failures = append(failures, common.Failure{ Text: fmt.Sprintf("the termination reason is %s exitCode=%d container=%s pod=%s", reason, exitCode, containerStatus.Name, name), Sensitive: []common.Sensitive{}, }) } } else { // when pod is Running but its ReadinessProbe fails if !containerStatus.Ready && statusPhase == "Running" { // parse the event log and append details evt, err := util.FetchLatestEvent(a.Context, a.Client, namespace, name) if err != nil || evt == nil { continue } if evt.Reason == "Unhealthy" && evt.Message != "" { failures = append(failures, common.Failure{ Text: evt.Message, Sensitive: []common.Sensitive{}, }) } } } } return failures } func isErrorReason(reason string) bool { failureReasons := []string{ "CrashLoopBackOff", "ImagePullBackOff", "CreateContainerConfigError", "PreCreateHookError", "CreateContainerError", "PreStartHookError", "RunContainerError", "ImageInspectError", "ErrImagePull", "ErrImageNeverPull", "InvalidImageName", } for _, r := range failureReasons { if r == reason { return true } } return false } func isEvtErrorReason(reason string) bool { failureReasons := []string{ "FailedCreatePodSandBox", "FailedMount", } for _, r := range failureReasons { if r == reason { return true } } return false } ================================================ FILE: pkg/analyzer/pod_test.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package analyzer import ( "context" "sort" "testing" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" "github.com/stretchr/testify/require" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/fake" ) func TestPodAnalyzer(t *testing.T) { tests := []struct { name string config common.Analyzer expectations []struct { name string failuresCount int } }{ { name: "Pending pods, namespace filtering and readiness probe failure", config: common.Analyzer{ Client: &kubernetes.Client{ Client: fake.NewSimpleClientset( &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "Pod1", Namespace: "default", }, Status: v1.PodStatus{ Phase: v1.PodPending, Conditions: []v1.PodCondition{ { // This condition will contribute to failures. Type: v1.PodScheduled, Reason: "Unschedulable", Message: "0/1 nodes are available: 1 node(s) had taint {node-role.kubernetes.io/master: }, that the pod didn't tolerate.", }, { // This condition won't contribute to failures. Type: v1.PodScheduled, Reason: "Unexpected failure", }, }, }, }, &v1.Pod{ // This pod won't be selected because of namespace filtering. ObjectMeta: metav1.ObjectMeta{ Name: "Pod2", Namespace: "test", }, }, &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "Pod3", Namespace: "default", }, Status: v1.PodStatus{ // When pod is Running but its ReadinessProbe fails Phase: v1.PodRunning, ContainerStatuses: []v1.ContainerStatus{ { Ready: false, }, }, }, }, &v1.Event{ ObjectMeta: metav1.ObjectMeta{ Name: "Event1", Namespace: "default", }, InvolvedObject: v1.ObjectReference{ Kind: "Pod", Name: "Pod3", Namespace: "default", }, Reason: "Unhealthy", Message: "readiness probe failed: the detail reason here ...", Source: v1.EventSource{Component: "eventTest"}, Count: 1, Type: v1.EventTypeWarning, }, ), }, Context: context.Background(), Namespace: "default", }, expectations: []struct { name string failuresCount int }{ { name: "default/Pod1", failuresCount: 1, }, { name: "default/Pod3", failuresCount: 1, }, }, }, { name: "readiness probe failure without any event", config: common.Analyzer{ Client: &kubernetes.Client{ Client: fake.NewSimpleClientset( &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "Pod1", Namespace: "default", }, Status: v1.PodStatus{ // When pod is Running but its ReadinessProbe fails // It won't contribute to any failures because // there's no event present. Phase: v1.PodRunning, ContainerStatuses: []v1.ContainerStatus{ { Ready: false, }, }, }, }, ), }, Context: context.Background(), Namespace: "default", }, }, { name: "Init container status state waiting", config: common.Analyzer{ Client: &kubernetes.Client{ Client: fake.NewSimpleClientset( &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "Pod1", Namespace: "default", }, Status: v1.PodStatus{ Phase: v1.PodPending, InitContainerStatuses: []v1.ContainerStatus{ { Ready: true, State: v1.ContainerState{ Running: &v1.ContainerStateRunning{ StartedAt: metav1.Now(), }, }, }, { Ready: false, State: v1.ContainerState{ Waiting: &v1.ContainerStateWaiting{ // This represents a container that is still being created or blocked due to conditions such as OOMKilled Reason: "ContainerCreating", }, }, }, }, }, }, &v1.Event{ ObjectMeta: metav1.ObjectMeta{ Name: "Event1", Namespace: "default", }, InvolvedObject: v1.ObjectReference{ Kind: "Pod", Name: "Pod1", Namespace: "default", }, Reason: "FailedCreatePodSandBox", Message: "failed to create the pod sandbox ...", Type: v1.EventTypeWarning, }, ), }, Context: context.Background(), Namespace: "default", }, expectations: []struct { name string failuresCount int }{ { name: "default/Pod1", failuresCount: 1, }, }, }, { name: "Container status state waiting but no event reported", config: common.Analyzer{ Client: &kubernetes.Client{ Client: fake.NewSimpleClientset( &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "Pod1", Namespace: "default", }, Status: v1.PodStatus{ Phase: v1.PodPending, ContainerStatuses: []v1.ContainerStatus{ { Ready: false, State: v1.ContainerState{ Waiting: &v1.ContainerStateWaiting{ // This represents a container that is still being created or blocked due to conditions such as OOMKilled Reason: "ContainerCreating", }, }, }, }, }, }, ), }, Context: context.Background(), Namespace: "default", }, }, { name: "Container status state waiting", config: common.Analyzer{ Client: &kubernetes.Client{ Client: fake.NewSimpleClientset( &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "Pod1", Namespace: "default", }, Status: v1.PodStatus{ Phase: v1.PodPending, ContainerStatuses: []v1.ContainerStatus{ { Name: "Container1", Ready: false, State: v1.ContainerState{ Waiting: &v1.ContainerStateWaiting{ // This represents a container that is still being created or blocked due to conditions such as OOMKilled Reason: "ContainerCreating", }, }, }, { Name: "Container2", Ready: false, State: v1.ContainerState{ Waiting: &v1.ContainerStateWaiting{ // This represents container that is in CrashLoopBackOff state due to conditions such as OOMKilled Reason: "CrashLoopBackOff", }, }, LastTerminationState: v1.ContainerState{ Terminated: &v1.ContainerStateTerminated{ Reason: "test reason", }, }, }, { Name: "Container3", Ready: false, State: v1.ContainerState{ Waiting: &v1.ContainerStateWaiting{ // This won't contribute to failures. Reason: "RandomReason", Message: "This container won't be present in the failures", }, }, }, { Name: "Container4", Ready: false, State: v1.ContainerState{ Waiting: &v1.ContainerStateWaiting{ // Valid error reason. Reason: "PreStartHookError", Message: "Container4 encountered PreStartHookError", }, }, }, { Name: "Container5", Ready: false, State: v1.ContainerState{ Waiting: &v1.ContainerStateWaiting{ // Valid error reason. Reason: "CrashLoopBackOff", Message: "Container4 encountered CrashLoopBackOff", }, }, }, }, }, }, &v1.Event{ ObjectMeta: metav1.ObjectMeta{ Name: "Event1", Namespace: "default", }, InvolvedObject: v1.ObjectReference{ Kind: "Pod", Name: "Pod1", Namespace: "default", }, // This reason won't contribute to failures. Reason: "RandomEvent", Type: v1.EventTypeWarning, }, ), }, Context: context.Background(), Namespace: "default", }, expectations: []struct { name string failuresCount int }{ { name: "default/Pod1", failuresCount: 3, }, }, }, { name: "Terminated container with non-zero exit code", config: common.Analyzer{ Client: &kubernetes.Client{ Client: fake.NewSimpleClientset( &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "Pod1", Namespace: "default", }, Status: v1.PodStatus{ Phase: v1.PodFailed, ContainerStatuses: []v1.ContainerStatus{ { Name: "Container1", Ready: false, State: v1.ContainerState{ Terminated: &v1.ContainerStateTerminated{ ExitCode: 1, Reason: "Error", }, }, }, { Name: "Container2", Ready: false, State: v1.ContainerState{ Terminated: &v1.ContainerStateTerminated{ ExitCode: 2, Reason: "", }, }, }, }, }, }, ), }, Context: context.Background(), Namespace: "default", }, expectations: []struct { name string failuresCount int }{ { name: "default/Pod1", failuresCount: 2, }, }, }, } podAnalyzer := PodAnalyzer{} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { results, err := podAnalyzer.Analyze(tt.config) require.NoError(t, err) if tt.expectations == nil { require.Equal(t, 0, len(results)) } else { sort.Slice(results, func(i, j int) bool { return results[i].Name < results[j].Name }) require.Equal(t, len(tt.expectations), len(results)) for i, result := range results { require.Equal(t, tt.expectations[i].name, result.Name) require.Equal(t, tt.expectations[i].failuresCount, len(result.Error)) } } }) } } ================================================ FILE: pkg/analyzer/pvc.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package analyzer import ( "fmt" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/util" appsv1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) type PvcAnalyzer struct{} func (PvcAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) { kind := "PersistentVolumeClaim" AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{ "analyzer_name": kind, }) // search all namespaces for pods that are not running list, err := a.Client.GetClient().CoreV1().PersistentVolumeClaims(a.Namespace).List(a.Context, metav1.ListOptions{LabelSelector: a.LabelSelector}) if err != nil { return nil, err } var preAnalysis = map[string]common.PreAnalysis{} for _, pvc := range list.Items { var failures []common.Failure // Check for empty rs if pvc.Status.Phase == appsv1.ClaimPending { // parse the event log and append details evt, err := util.FetchLatestEvent(a.Context, a.Client, pvc.Namespace, pvc.Name) if err != nil || evt == nil { continue } if evt.Reason == "ProvisioningFailed" && evt.Message != "" { failures = append(failures, common.Failure{ Text: evt.Message, Sensitive: []common.Sensitive{}, }) } } if len(failures) > 0 { preAnalysis[fmt.Sprintf("%s/%s", pvc.Namespace, pvc.Name)] = common.PreAnalysis{ PersistentVolumeClaim: pvc, FailureDetails: failures, } AnalyzerErrorsMetric.WithLabelValues(kind, pvc.Name, pvc.Namespace).Set(float64(len(failures))) } } for key, value := range preAnalysis { var currentAnalysis = common.Result{ Kind: kind, Name: key, Error: value.FailureDetails, } parent, found := util.GetParent(a.Client, value.PersistentVolumeClaim.ObjectMeta) if found { currentAnalysis.ParentObject = parent } a.Results = append(a.Results, currentAnalysis) } return a.Results, nil } ================================================ FILE: pkg/analyzer/pvc_test.go ================================================ /* Copyright 2024 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package analyzer import ( "context" "sort" "testing" "time" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" "github.com/stretchr/testify/require" appsv1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/fake" ) func TestPersistentVolumeClaimAnalyzer(t *testing.T) { tests := []struct { name string config common.Analyzer expectations []string }{ { name: "PV1 and PVC5 report failures", config: common.Analyzer{ Client: &kubernetes.Client{ Client: fake.NewSimpleClientset( &appsv1.Event{ ObjectMeta: metav1.ObjectMeta{ Name: "Event1", Namespace: "default", }, LastTimestamp: metav1.Time{ Time: time.Date(2024, 3, 15, 10, 0, 0, 0, time.UTC), }, Reason: "ProvisioningFailed", Message: "PVC Event1 provisioning failed", }, &appsv1.Event{ ObjectMeta: metav1.ObjectMeta{ // This event won't get selected. Name: "Event2", Namespace: "test", }, }, &appsv1.Event{ // This is the latest event. ObjectMeta: metav1.ObjectMeta{ Name: "Event3", Namespace: "default", }, LastTimestamp: metav1.Time{ Time: time.Date(2024, 4, 15, 10, 0, 0, 0, time.UTC), }, Reason: "ProvisioningFailed", Message: "PVC Event3 provisioning failed", }, &appsv1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "PVC1", Namespace: "default", }, Status: appsv1.PersistentVolumeClaimStatus{ Phase: appsv1.ClaimPending, }, }, &appsv1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "PVC2", Namespace: "default", }, Status: appsv1.PersistentVolumeClaimStatus{ // Won't contribute to failures. Phase: appsv1.ClaimBound, }, }, &appsv1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "PVC3", Namespace: "default", }, Status: appsv1.PersistentVolumeClaimStatus{ // Won't contribute to failures. Phase: appsv1.ClaimLost, }, }, &appsv1.PersistentVolumeClaim{ // PVCs in namespace other than "default" won't be discovered. ObjectMeta: metav1.ObjectMeta{ Name: "PVC4", Namespace: "test", }, Status: appsv1.PersistentVolumeClaimStatus{ Phase: appsv1.ClaimLost, }, }, &appsv1.PersistentVolumeClaim{ // PVCs in namespace other than "default" won't be discovered. ObjectMeta: metav1.ObjectMeta{ Name: "PVC5", Namespace: "default", }, Status: appsv1.PersistentVolumeClaimStatus{ Phase: appsv1.ClaimPending, }, }, ), }, Context: context.Background(), Namespace: "default", }, expectations: []string{ "default/PVC1", "default/PVC5", }, }, { name: "no event", config: common.Analyzer{ Client: &kubernetes.Client{ Client: fake.NewSimpleClientset( &appsv1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "PVC1", Namespace: "default", }, Status: appsv1.PersistentVolumeClaimStatus{ Phase: appsv1.ClaimPending, }, }, ), }, Context: context.Background(), Namespace: "default", }, }, { name: "event other than provision failure", config: common.Analyzer{ Client: &kubernetes.Client{ Client: fake.NewSimpleClientset( &appsv1.Event{ ObjectMeta: metav1.ObjectMeta{ Name: "Event1", Namespace: "default", }, // Any reason other than ProvisioningFailed won't result in failure. Reason: "UnknownReason", }, &appsv1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "PVC1", Namespace: "default", }, Status: appsv1.PersistentVolumeClaimStatus{ Phase: appsv1.ClaimPending, }, }, ), }, Context: context.Background(), Namespace: "default", }, }, { name: "event without error message", config: common.Analyzer{ Client: &kubernetes.Client{ Client: fake.NewSimpleClientset( &appsv1.Event{ ObjectMeta: metav1.ObjectMeta{ Name: "Event1", Namespace: "default", }, // Event without any error message won't result in failure. Reason: "ProvisioningFailed", }, &appsv1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "PVC1", Namespace: "default", }, Status: appsv1.PersistentVolumeClaimStatus{ Phase: appsv1.ClaimPending, }, }, ), }, Context: context.Background(), Namespace: "default", }, }, } pvcAnalyzer := PvcAnalyzer{} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { results, err := pvcAnalyzer.Analyze(tt.config) require.NoError(t, err) if tt.expectations == nil { require.Equal(t, 0, len(results)) } else { sort.Slice(results, func(i, j int) bool { return results[i].Name < results[j].Name }) require.Equal(t, len(tt.expectations), len(results)) for i, expectation := range tt.expectations { require.Equal(t, expectation, results[i].Name) } } }) } } func TestPvcAnalyzerLabelSelectorFiltering(t *testing.T) { config := common.Analyzer{ Client: &kubernetes.Client{ Client: fake.NewSimpleClientset( &appsv1.Event{ ObjectMeta: metav1.ObjectMeta{ Name: "Event1", Namespace: "default", }, LastTimestamp: metav1.Time{ Time: time.Date(2024, 3, 15, 10, 0, 0, 0, time.UTC), }, Reason: "ProvisioningFailed", Message: "PVC Event1 provisioning failed", }, &appsv1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "PVC1", Namespace: "default", Labels: map[string]string{ "app": "pvc", }, }, Status: appsv1.PersistentVolumeClaimStatus{ Phase: appsv1.ClaimPending, }, }, &appsv1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "PVC2", Namespace: "default", }, Status: appsv1.PersistentVolumeClaimStatus{ Phase: appsv1.ClaimPending, }, }, ), }, Context: context.Background(), Namespace: "default", LabelSelector: "app=pvc", } pvcAnalyzer := PvcAnalyzer{} results, err := pvcAnalyzer.Analyze(config) require.NoError(t, err) require.Equal(t, 1, len(results)) require.Equal(t, "default/PVC1", results[0].Name) } ================================================ FILE: pkg/analyzer/rs.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package analyzer import ( "fmt" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/util" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) type ReplicaSetAnalyzer struct{} func (ReplicaSetAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) { kind := "ReplicaSet" AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{ "analyzer_name": kind, }) // search all namespaces for pods that are not running list, err := a.Client.GetClient().AppsV1().ReplicaSets(a.Namespace).List(a.Context, metav1.ListOptions{LabelSelector: a.LabelSelector}) if err != nil { return nil, err } var preAnalysis = map[string]common.PreAnalysis{} for _, rs := range list.Items { var failures []common.Failure // Check for empty rs if rs.Status.Replicas == 0 { // Check through container status to check for crashes for _, rsStatus := range rs.Status.Conditions { if rsStatus.Type == "ReplicaFailure" && rsStatus.Reason == "FailedCreate" { failures = append(failures, common.Failure{ Text: rsStatus.Message, Sensitive: []common.Sensitive{}, }) } } } if len(failures) > 0 { preAnalysis[fmt.Sprintf("%s/%s", rs.Namespace, rs.Name)] = common.PreAnalysis{ ReplicaSet: rs, FailureDetails: failures, } AnalyzerErrorsMetric.WithLabelValues(kind, rs.Name, rs.Namespace).Set(float64(len(failures))) } } for key, value := range preAnalysis { var currentAnalysis = common.Result{ Kind: kind, Name: key, Error: value.FailureDetails, } parent, found := util.GetParent(a.Client, value.ReplicaSet.ObjectMeta) if found { currentAnalysis.ParentObject = parent } a.Results = append(a.Results, currentAnalysis) } return a.Results, nil } ================================================ FILE: pkg/analyzer/rs_test.go ================================================ /* Copyright 2024 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package analyzer import ( "context" "sort" "testing" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" "github.com/stretchr/testify/require" appsv1 "k8s.io/api/apps/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/fake" ) func TestReplicaSetAnalyzer(t *testing.T) { config := common.Analyzer{ Client: &kubernetes.Client{ Client: fake.NewSimpleClientset( &appsv1.ReplicaSet{ ObjectMeta: metav1.ObjectMeta{ Name: "ReplicaSet1", Namespace: "default", }, Status: appsv1.ReplicaSetStatus{ Replicas: 0, Conditions: []appsv1.ReplicaSetCondition{ { // Should contribute to failures. Type: appsv1.ReplicaSetReplicaFailure, Reason: "FailedCreate", Message: "failed to create test replica set 1", }, }, }, }, &appsv1.ReplicaSet{ // This replicaset won't be discovered as it is not in the // default namespace. ObjectMeta: metav1.ObjectMeta{ Name: "ReplicaSet2", Namespace: "test", }, }, &appsv1.ReplicaSet{ ObjectMeta: metav1.ObjectMeta{ Name: "ReplicaSet3", Namespace: "default", }, Status: appsv1.ReplicaSetStatus{ Replicas: 0, Conditions: []appsv1.ReplicaSetCondition{ { Type: appsv1.ReplicaSetReplicaFailure, // Should not be included in the failures. Reason: "RandomError", }, }, }, }, &appsv1.ReplicaSet{ ObjectMeta: metav1.ObjectMeta{ Name: "ReplicaSet4", Namespace: "default", }, Status: appsv1.ReplicaSetStatus{ Replicas: 0, Conditions: []appsv1.ReplicaSetCondition{ { // Should contribute to failures. Type: appsv1.ReplicaSetReplicaFailure, Reason: "FailedCreate", Message: "failed to create test replica set 4 condition 1", }, { // Should not contribute to failures. Type: appsv1.ReplicaSetReplicaFailure, Reason: "Unknown", }, { // Should not contribute to failures. Type: appsv1.ReplicaSetReplicaFailure, Reason: "FailedCreate", Message: "failed to create test replica set 4 condition 3", }, }, }, }, &appsv1.ReplicaSet{ // Replicaset without any failures. ObjectMeta: metav1.ObjectMeta{ Name: "ReplicaSet5", Namespace: "default", }, Status: appsv1.ReplicaSetStatus{ Replicas: 3, }, }, ), }, Context: context.Background(), Namespace: "default", } rsAnalyzer := ReplicaSetAnalyzer{} results, err := rsAnalyzer.Analyze(config) require.NoError(t, err) sort.Slice(results, func(i, j int) bool { return results[i].Name < results[j].Name }) expectations := []struct { name string failuresCount int }{ { name: "default/ReplicaSet1", failuresCount: 1, }, { name: "default/ReplicaSet4", failuresCount: 2, }, } require.Equal(t, len(expectations), len(results)) for i, result := range results { require.Equal(t, expectations[i].name, result.Name) require.Equal(t, expectations[i].failuresCount, len(result.Error)) } } func TestReplicaSetAnalyzerLabelSelectorFiltering(t *testing.T) { config := common.Analyzer{ Client: &kubernetes.Client{ Client: fake.NewSimpleClientset( &appsv1.ReplicaSet{ ObjectMeta: metav1.ObjectMeta{ Name: "ReplicaSet1", Namespace: "default", Labels: map[string]string{ "app": "replicaset", }, }, Status: appsv1.ReplicaSetStatus{ Replicas: 0, Conditions: []appsv1.ReplicaSetCondition{ { // Should contribute to failures. Type: appsv1.ReplicaSetReplicaFailure, Reason: "FailedCreate", Message: "failed to create test replica set 1", }, }, }, }, &appsv1.ReplicaSet{ ObjectMeta: metav1.ObjectMeta{ Name: "ReplicaSet2", Namespace: "default", }, Status: appsv1.ReplicaSetStatus{ Replicas: 0, Conditions: []appsv1.ReplicaSetCondition{ { // Should contribute to failures. Type: appsv1.ReplicaSetReplicaFailure, Reason: "FailedCreate", Message: "failed to create test replica set 1", }, }, }, }, ), }, Context: context.Background(), Namespace: "default", LabelSelector: "app=replicaset", } rsAnalyzer := ReplicaSetAnalyzer{} results, err := rsAnalyzer.Analyze(config) require.NoError(t, err) require.Equal(t, 1, len(results)) require.Equal(t, "default/ReplicaSet1", results[0].Name) } ================================================ FILE: pkg/analyzer/security.go ================================================ /* Copyright 2024 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package analyzer import ( "fmt" "github.com/k8sgpt-ai/k8sgpt/pkg/common" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) type SecurityAnalyzer struct{} func (SecurityAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) { kind := "Security" AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{ "analyzer_name": kind, }) var results []common.Result // Analyze ServiceAccounts saResults, err := analyzeServiceAccounts(a) if err != nil { return nil, err } results = append(results, saResults...) // Analyze RoleBindings rbResults, err := analyzeRoleBindings(a) if err != nil { return nil, err } results = append(results, rbResults...) // Analyze Pod Security Contexts podResults, err := analyzePodSecurityContexts(a) if err != nil { return nil, err } results = append(results, podResults...) return results, nil } func analyzeServiceAccounts(a common.Analyzer) ([]common.Result, error) { var results []common.Result sas, err := a.Client.GetClient().CoreV1().ServiceAccounts(a.Namespace).List(a.Context, metav1.ListOptions{ LabelSelector: a.LabelSelector, }) if err != nil { return nil, err } for _, sa := range sas.Items { var failures []common.Failure // Check for default service account usage if sa.Name == "default" { pods, err := a.Client.GetClient().CoreV1().Pods(sa.Namespace).List(a.Context, metav1.ListOptions{}) if err != nil { continue } defaultSAUsers := []string{} for _, pod := range pods.Items { if pod.Spec.ServiceAccountName == "default" { defaultSAUsers = append(defaultSAUsers, pod.Name) } } if len(defaultSAUsers) > 0 { failures = append(failures, common.Failure{ Text: fmt.Sprintf("Default service account is being used by pods: %v", defaultSAUsers), Sensitive: []common.Sensitive{}, }) } } if len(failures) > 0 { results = append(results, common.Result{ Kind: "Security/ServiceAccount", Name: fmt.Sprintf("%s/%s", sa.Namespace, sa.Name), Error: failures, }) AnalyzerErrorsMetric.WithLabelValues("Security/ServiceAccount", sa.Name, sa.Namespace).Set(float64(len(failures))) } } return results, nil } func analyzeRoleBindings(a common.Analyzer) ([]common.Result, error) { var results []common.Result rbs, err := a.Client.GetClient().RbacV1().RoleBindings(a.Namespace).List(a.Context, metav1.ListOptions{ LabelSelector: a.LabelSelector, }) if err != nil { return nil, err } for _, rb := range rbs.Items { var failures []common.Failure // Check for wildcards in role references role, err := a.Client.GetClient().RbacV1().Roles(rb.Namespace).Get(a.Context, rb.RoleRef.Name, metav1.GetOptions{}) if err != nil { continue } for _, rule := range role.Rules { if containsWildcard(rule.Verbs) || containsWildcard(rule.Resources) { failures = append(failures, common.Failure{ Text: fmt.Sprintf("RoleBinding %s references Role %s which contains wildcard permissions - this is not recommended for security best practices", rb.Name, role.Name), Sensitive: []common.Sensitive{}, }) } } if len(failures) > 0 { results = append(results, common.Result{ Kind: "Security/RoleBinding", Name: fmt.Sprintf("%s/%s", rb.Namespace, rb.Name), Error: failures, }) AnalyzerErrorsMetric.WithLabelValues("Security/RoleBinding", rb.Name, rb.Namespace).Set(float64(len(failures))) } } return results, nil } func analyzePodSecurityContexts(a common.Analyzer) ([]common.Result, error) { var results []common.Result pods, err := a.Client.GetClient().CoreV1().Pods(a.Namespace).List(a.Context, metav1.ListOptions{ LabelSelector: a.LabelSelector, }) if err != nil { return nil, err } for _, pod := range pods.Items { var failures []common.Failure // Check for privileged containers first (most critical) hasPrivilegedContainer := false for _, container := range pod.Spec.Containers { if container.SecurityContext != nil && container.SecurityContext.Privileged != nil && *container.SecurityContext.Privileged { failures = append(failures, common.Failure{ Text: fmt.Sprintf("Container %s in pod %s is running as privileged which poses security risks", container.Name, pod.Name), Sensitive: []common.Sensitive{}, }) hasPrivilegedContainer = true break } } // Only check for missing security context if no privileged containers found if !hasPrivilegedContainer && pod.Spec.SecurityContext == nil { failures = append(failures, common.Failure{ Text: fmt.Sprintf("Pod %s does not have a security context defined which may pose security risks", pod.Name), Sensitive: []common.Sensitive{}, }) } if len(failures) > 0 { results = append(results, common.Result{ Kind: "Security/Pod", Name: fmt.Sprintf("%s/%s", pod.Namespace, pod.Name), Error: failures[:1], }) AnalyzerErrorsMetric.WithLabelValues("Security/Pod", pod.Name, pod.Namespace).Set(1) } } return results, nil } func containsWildcard(slice []string) bool { for _, item := range slice { if item == "*" { return true } } return false } ================================================ FILE: pkg/analyzer/security_test.go ================================================ /* Copyright 2024 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package analyzer import ( "context" "testing" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" "github.com/stretchr/testify/assert" v1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/fake" ) func TestSecurityAnalyzer(t *testing.T) { tests := []struct { name string namespace string serviceAccounts []v1.ServiceAccount pods []v1.Pod roles []rbacv1.Role roleBindings []rbacv1.RoleBinding expectedErrors int expectedKinds []string }{ { name: "default service account usage", namespace: "default", serviceAccounts: []v1.ServiceAccount{ { ObjectMeta: metav1.ObjectMeta{ Name: "default", Namespace: "default", }, }, }, pods: []v1.Pod{ { ObjectMeta: metav1.ObjectMeta{ Name: "test-pod", Namespace: "default", }, Spec: v1.PodSpec{ ServiceAccountName: "default", }, }, }, expectedErrors: 2, expectedKinds: []string{"Security/ServiceAccount", "Security/Pod"}, }, { name: "privileged container", namespace: "default", pods: []v1.Pod{ { ObjectMeta: metav1.ObjectMeta{ Name: "privileged-pod", Namespace: "default", }, Spec: v1.PodSpec{ Containers: []v1.Container{ { Name: "privileged-container", SecurityContext: &v1.SecurityContext{ Privileged: boolPtr(true), }, }, }, }, }, }, expectedErrors: 1, expectedKinds: []string{"Security/Pod"}, }, { name: "wildcard permissions in role", namespace: "default", roles: []rbacv1.Role{ { ObjectMeta: metav1.ObjectMeta{ Name: "wildcard-role", Namespace: "default", }, Rules: []rbacv1.PolicyRule{ { Verbs: []string{"*"}, Resources: []string{"pods"}, }, }, }, }, roleBindings: []rbacv1.RoleBinding{ { ObjectMeta: metav1.ObjectMeta{ Name: "test-binding", Namespace: "default", }, RoleRef: rbacv1.RoleRef{ Kind: "Role", Name: "wildcard-role", }, }, }, expectedErrors: 1, expectedKinds: []string{"Security/RoleBinding"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { client := fake.NewSimpleClientset() // Create test resources for _, sa := range tt.serviceAccounts { _, err := client.CoreV1().ServiceAccounts(tt.namespace).Create(context.TODO(), &sa, metav1.CreateOptions{}) assert.NoError(t, err) } for _, pod := range tt.pods { _, err := client.CoreV1().Pods(tt.namespace).Create(context.TODO(), &pod, metav1.CreateOptions{}) assert.NoError(t, err) } for _, role := range tt.roles { _, err := client.RbacV1().Roles(tt.namespace).Create(context.TODO(), &role, metav1.CreateOptions{}) assert.NoError(t, err) } for _, rb := range tt.roleBindings { _, err := client.RbacV1().RoleBindings(tt.namespace).Create(context.TODO(), &rb, metav1.CreateOptions{}) assert.NoError(t, err) } analyzer := SecurityAnalyzer{} results, err := analyzer.Analyze(common.Analyzer{ Client: &kubernetes.Client{Client: client}, Context: context.TODO(), Namespace: tt.namespace, }) assert.NoError(t, err) // Debug: Print all results t.Logf("Got %d results:", len(results)) for _, result := range results { t.Logf(" Kind: %s, Name: %s", result.Kind, result.Name) for _, failure := range result.Error { t.Logf(" Failure: %s", failure.Text) } } // Count results by kind resultsByKind := make(map[string]int) for _, result := range results { resultsByKind[result.Kind]++ } // Check that we have the expected number of results for each kind for _, expectedKind := range tt.expectedKinds { assert.Equal(t, 1, resultsByKind[expectedKind], "Expected 1 result of kind %s", expectedKind) } // Check total number of results matches expected kinds assert.Equal(t, len(tt.expectedKinds), len(results), "Expected %d total results", len(tt.expectedKinds)) }) } } ================================================ FILE: pkg/analyzer/service.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package analyzer import ( "fmt" "github.com/fatih/color" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/tools/leaderelection/resourcelock" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" "github.com/k8sgpt-ai/k8sgpt/pkg/util" "k8s.io/apimachinery/pkg/runtime/schema" ) type ServiceAnalyzer struct{} func (ServiceAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) { kind := "Service" apiDoc := kubernetes.K8sApiReference{ Kind: kind, ApiVersion: schema.GroupVersion{ Group: "", Version: "v1", }, OpenapiSchema: a.OpenapiSchema, } AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{ "analyzer_name": kind, }) // search all namespaces for pods that are not running list, err := a.Client.GetClient().CoreV1().Endpoints(a.Namespace).List(a.Context, metav1.ListOptions{LabelSelector: a.LabelSelector}) if err != nil { return nil, err } var preAnalysis = map[string]common.PreAnalysis{} for _, ep := range list.Items { var failures []common.Failure // Check for empty service if len(ep.Subsets) == 0 { if _, ok := ep.Annotations[resourcelock.LeaderElectionRecordAnnotationKey]; ok { continue } svc, err := a.Client.GetClient().CoreV1().Services(ep.Namespace).Get(a.Context, ep.Name, metav1.GetOptions{}) if err != nil { color.Yellow("Service %s/%s does not exist", ep.Namespace, ep.Name) continue } for k, v := range svc.Spec.Selector { doc := apiDoc.GetApiDocV2("spec.selector") failures = append(failures, common.Failure{ Text: fmt.Sprintf("Service has no endpoints, expected label %s=%s", k, v), KubernetesDoc: doc, Sensitive: []common.Sensitive{ { Unmasked: k, Masked: util.MaskString(k), }, { Unmasked: v, Masked: util.MaskString(v), }, }, }) } } else { count := 0 pods := []string{} // Check through container status to check for crashes for _, epSubset := range ep.Subsets { apiDoc.Kind = "Endpoints" if len(epSubset.NotReadyAddresses) > 0 { for _, addresses := range epSubset.NotReadyAddresses { count++ pods = append(pods, addresses.TargetRef.Kind+"/"+addresses.TargetRef.Name) } } } if count > 0 { doc := apiDoc.GetApiDocV2("subsets.notReadyAddresses") failures = append(failures, common.Failure{ Text: fmt.Sprintf("Service has not ready endpoints, pods: %s, expected %d", pods, count), KubernetesDoc: doc, Sensitive: []common.Sensitive{}, }) } } // fetch event events, err := a.Client.GetClient().CoreV1().Events(a.Namespace).List(a.Context, metav1.ListOptions{ FieldSelector: "involvedObject.name=" + ep.Name, }) if err != nil { return nil, err } for _, event := range events.Items { if event.Type != "Normal" { failures = append(failures, common.Failure{ Text: fmt.Sprintf("Service %s/%s has event %s", ep.Namespace, ep.Name, event.Message), }) } } if len(failures) > 0 { preAnalysis[fmt.Sprintf("%s/%s", ep.Namespace, ep.Name)] = common.PreAnalysis{ Endpoint: ep, FailureDetails: failures, } AnalyzerErrorsMetric.WithLabelValues(kind, ep.Name, ep.Namespace).Set(float64(len(failures))) } } for key, value := range preAnalysis { var currentAnalysis = common.Result{ Kind: kind, Name: key, Error: value.FailureDetails, } parent, found := util.GetParent(a.Client, value.Endpoint.ObjectMeta) if found { currentAnalysis.ParentObject = parent } a.Results = append(a.Results, currentAnalysis) } return a.Results, nil } ================================================ FILE: pkg/analyzer/service_test.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package analyzer import ( "context" "sort" "testing" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" "github.com/stretchr/testify/require" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/fake" ) func TestServiceAnalyzer(t *testing.T) { tests := []struct { name string config common.Analyzer expectations []struct { name string failuresCount int } }{ { name: "Service with no endpoints", config: common.Analyzer{ Client: &kubernetes.Client{ Client: fake.NewSimpleClientset( &v1.Endpoints{ ObjectMeta: metav1.ObjectMeta{ Name: "test-service", Namespace: "default", }, Subsets: []v1.EndpointSubset{}, // Empty subsets }, &v1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "test-service", Namespace: "default", }, Spec: v1.ServiceSpec{ Selector: map[string]string{ "app": "test", }, }, }, ), }, Namespace: "default", }, expectations: []struct { name string failuresCount int }{ { name: "default/test-service", failuresCount: 1, // One failure for no endpoints }, }, }, { name: "Service with not ready endpoints", config: common.Analyzer{ Client: &kubernetes.Client{ Client: fake.NewSimpleClientset( &v1.Endpoints{ ObjectMeta: metav1.ObjectMeta{ Name: "test-service", Namespace: "default", }, Subsets: []v1.EndpointSubset{ { NotReadyAddresses: []v1.EndpointAddress{ { TargetRef: &v1.ObjectReference{ Kind: "Pod", Name: "test-pod", }, }, }, }, }, }, &v1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "test-service", Namespace: "default", }, Spec: v1.ServiceSpec{ Selector: map[string]string{ "app": "test", }, }, }, ), }, Namespace: "default", }, expectations: []struct { name string failuresCount int }{ { name: "default/test-service", failuresCount: 1, // One failure for not ready endpoints }, }, }, { name: "Service with warning events", config: common.Analyzer{ Client: &kubernetes.Client{ Client: fake.NewSimpleClientset( &v1.Endpoints{ ObjectMeta: metav1.ObjectMeta{ Name: "test-service", Namespace: "default", }, Subsets: []v1.EndpointSubset{}, // Empty subsets }, &v1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "test-service", Namespace: "default", }, Spec: v1.ServiceSpec{ Selector: map[string]string{ "app": "test", }, }, }, &v1.Event{ ObjectMeta: metav1.ObjectMeta{ Name: "test-event", Namespace: "default", }, InvolvedObject: v1.ObjectReference{ Kind: "Service", Name: "test-service", Namespace: "default", }, Type: "Warning", Reason: "TestReason", Message: "Test warning message", }, ), }, Namespace: "default", }, expectations: []struct { name string failuresCount int }{ { name: "default/test-service", failuresCount: 2, // One failure for no endpoints, one for warning event }, }, }, { name: "Service with leader election annotation", config: common.Analyzer{ Client: &kubernetes.Client{ Client: fake.NewSimpleClientset( &v1.Endpoints{ ObjectMeta: metav1.ObjectMeta{ Name: "test-service", Namespace: "default", Annotations: map[string]string{ "control-plane.alpha.kubernetes.io/leader": "test-leader", }, }, Subsets: []v1.EndpointSubset{}, // Empty subsets }, &v1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "test-service", Namespace: "default", }, Spec: v1.ServiceSpec{ Selector: map[string]string{ "app": "test", }, }, }, ), }, Namespace: "default", }, expectations: []struct { name string failuresCount int }{ // No expectations for leader election endpoints }, }, { name: "Service with non-existent service", config: common.Analyzer{ Client: &kubernetes.Client{ Client: fake.NewSimpleClientset( &v1.Endpoints{ ObjectMeta: metav1.ObjectMeta{ Name: "test-service", Namespace: "default", }, Subsets: []v1.EndpointSubset{}, // Empty subsets }, ), }, Namespace: "default", }, expectations: []struct { name string failuresCount int }{ // No expectations for non-existent service }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { analyzer := ServiceAnalyzer{} results, err := analyzer.Analyze(tt.config) require.NoError(t, err) require.Len(t, results, len(tt.expectations)) // Sort results by name for consistent comparison sort.Slice(results, func(i, j int) bool { return results[i].Name < results[j].Name }) for i, expectation := range tt.expectations { require.Equal(t, expectation.name, results[i].Name) require.Len(t, results[i].Error, expectation.failuresCount) } }) } } func TestServiceAnalyzerLabelSelectorFiltering(t *testing.T) { clientSet := fake.NewSimpleClientset( &v1.Endpoints{ ObjectMeta: metav1.ObjectMeta{ Name: "Endpoint1", Namespace: "default", Labels: map[string]string{ "app": "service", "part-of": "test", }, }, // Endpoint with non-zero subsets. Subsets: []v1.EndpointSubset{ { // These not ready end points will contribute to failures. NotReadyAddresses: []v1.EndpointAddress{ { TargetRef: &v1.ObjectReference{ Kind: "test-reference", Name: "reference1", }, }, { TargetRef: &v1.ObjectReference{ Kind: "test-reference", Name: "reference2", }, }, }, }, { // These not ready end points will contribute to failures. NotReadyAddresses: []v1.EndpointAddress{ { TargetRef: &v1.ObjectReference{ Kind: "test-reference", Name: "reference3", }, }, }, }, }, }, &v1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "Service1", Namespace: "default", Labels: map[string]string{ "app": "service", }, }, Spec: v1.ServiceSpec{ Selector: map[string]string{ "app1": "test-app1", "app2": "test-app2", }, }, }, &v1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "Service2", Namespace: "default", }, Spec: v1.ServiceSpec{ Selector: map[string]string{ "app1": "test-app1", "app2": "test-app2", }, }, }, ) config := common.Analyzer{ Client: &kubernetes.Client{ Client: clientSet, }, Context: context.Background(), Namespace: "default", LabelSelector: "app=service", } sAnalyzer := ServiceAnalyzer{} results, err := sAnalyzer.Analyze(config) require.NoError(t, err) require.Equal(t, 1, len(results)) require.Equal(t, "default/Endpoint1", results[0].Name) config = common.Analyzer{ Client: &kubernetes.Client{ Client: clientSet, }, Context: context.Background(), Namespace: "default", LabelSelector: "app=service,part-of=test", } sAnalyzer = ServiceAnalyzer{} results, err = sAnalyzer.Analyze(config) require.NoError(t, err) require.Equal(t, 1, len(results)) require.Equal(t, "default/Endpoint1", results[0].Name) } ================================================ FILE: pkg/analyzer/statefulset.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package analyzer import ( "fmt" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" "github.com/k8sgpt-ai/k8sgpt/pkg/util" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" ) type StatefulSetAnalyzer struct{} func (StatefulSetAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) { kind := "StatefulSet" apiDoc := kubernetes.K8sApiReference{ Kind: kind, ApiVersion: schema.GroupVersion{ Group: "apps", Version: "v1", }, OpenapiSchema: a.OpenapiSchema, } AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{ "analyzer_name": kind, }) list, err := a.Client.GetClient().AppsV1().StatefulSets(a.Namespace).List(a.Context, metav1.ListOptions{LabelSelector: a.LabelSelector}) if err != nil { return nil, err } var preAnalysis = map[string]common.PreAnalysis{} for _, sts := range list.Items { var failures []common.Failure // get serviceName serviceName := sts.Spec.ServiceName _, err := a.Client.GetClient().CoreV1().Services(sts.Namespace).Get(a.Context, serviceName, metav1.GetOptions{}) if err != nil { doc := apiDoc.GetApiDocV2("spec.serviceName") failures = append(failures, common.Failure{ Text: fmt.Sprintf( "StatefulSet uses the service %s/%s which does not exist.", sts.Namespace, serviceName, ), KubernetesDoc: doc, Sensitive: []common.Sensitive{ { Unmasked: sts.Namespace, Masked: util.MaskString(sts.Namespace), }, { Unmasked: serviceName, Masked: util.MaskString(serviceName), }, }, }) } if len(sts.Spec.VolumeClaimTemplates) > 0 { for _, volumeClaimTemplate := range sts.Spec.VolumeClaimTemplates { if volumeClaimTemplate.Spec.StorageClassName != nil { _, err := a.Client.GetClient().StorageV1().StorageClasses().Get(a.Context, *volumeClaimTemplate.Spec.StorageClassName, metav1.GetOptions{}) if err != nil { failures = append(failures, common.Failure{ Text: fmt.Sprintf("StatefulSet uses the storage class %s which does not exist.", *volumeClaimTemplate.Spec.StorageClassName), Sensitive: []common.Sensitive{ { Unmasked: *volumeClaimTemplate.Spec.StorageClassName, Masked: util.MaskString(*volumeClaimTemplate.Spec.StorageClassName), }, }, }) } } } } if sts.Spec.Replicas != nil && *(sts.Spec.Replicas) != sts.Status.AvailableReplicas { for i := int32(0); i < *(sts.Spec.Replicas); i++ { podName := sts.Name + "-" + fmt.Sprint(i) pod, err := a.Client.GetClient().CoreV1().Pods(sts.Namespace).Get(a.Context, podName, metav1.GetOptions{}) if err != nil { if errors.IsNotFound(err) && i == 0 { evt, err := util.FetchLatestEvent(a.Context, a.Client, sts.Namespace, sts.Name) if err != nil || evt == nil || evt.Type == "Normal" { break } failures = append(failures, common.Failure{ Text: evt.Message, Sensitive: []common.Sensitive{}, }) } break } if pod.Status.Phase != "Running" { failures = append(failures, common.Failure{ Text: fmt.Sprintf("Statefulset pod %s in the namespace %s is not in running state.", pod.Name, pod.Namespace), Sensitive: []common.Sensitive{ { Unmasked: sts.Namespace, Masked: util.MaskString(pod.Name), }, { Unmasked: serviceName, Masked: util.MaskString(pod.Namespace), }, }, }) break } } } if len(failures) > 0 { preAnalysis[fmt.Sprintf("%s/%s", sts.Namespace, sts.Name)] = common.PreAnalysis{ StatefulSet: sts, FailureDetails: failures, } AnalyzerErrorsMetric.WithLabelValues(kind, sts.Name, sts.Namespace).Set(float64(len(failures))) } } for key, value := range preAnalysis { var currentAnalysis = common.Result{ Kind: kind, Name: key, Error: value.FailureDetails, } parent, found := util.GetParent(a.Client, value.StatefulSet.ObjectMeta) if found { currentAnalysis.ParentObject = parent } a.Results = append(a.Results, currentAnalysis) } return a.Results, nil } ================================================ FILE: pkg/analyzer/statefulset_test.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package analyzer import ( "context" "testing" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" "github.com/magiconair/properties/assert" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/fake" ) func TestStatefulSetAnalyzer(t *testing.T) { clientset := fake.NewSimpleClientset( &appsv1.StatefulSet{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "default", }, }) statefulSetAnalyzer := StatefulSetAnalyzer{} config := common.Analyzer{ Client: &kubernetes.Client{ Client: clientset, }, Context: context.Background(), Namespace: "default", } analysisResults, err := statefulSetAnalyzer.Analyze(config) if err != nil { t.Error(err) } assert.Equal(t, len(analysisResults), 1) } func TestStatefulSetAnalyzerWithoutService(t *testing.T) { clientset := fake.NewSimpleClientset( &appsv1.StatefulSet{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "default", }, Spec: appsv1.StatefulSetSpec{ ServiceName: "example-svc", }, }) statefulSetAnalyzer := StatefulSetAnalyzer{} config := common.Analyzer{ Client: &kubernetes.Client{ Client: clientset, }, Context: context.Background(), Namespace: "default", } analysisResults, err := statefulSetAnalyzer.Analyze(config) if err != nil { t.Error(err) } var errorFound bool want := "StatefulSet uses the service default/example-svc which does not exist." for _, analysis := range analysisResults { for _, got := range analysis.Error { if want == got.Text { errorFound = true } } if errorFound { break } } if !errorFound { t.Errorf("Error expected: '%v', not found in StatefulSet's analysis results", want) } } func TestStatefulSetAnalyzerMissingStorageClass(t *testing.T) { storageClassName := "example-sc" clientset := fake.NewSimpleClientset( &appsv1.StatefulSet{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "default", }, Spec: appsv1.StatefulSetSpec{ ServiceName: "example-svc", VolumeClaimTemplates: []corev1.PersistentVolumeClaim{ { TypeMeta: metav1.TypeMeta{ Kind: "PersistentVolumeClaim", APIVersion: "v1", }, ObjectMeta: metav1.ObjectMeta{ Name: "pvc-example", }, Spec: corev1.PersistentVolumeClaimSpec{ StorageClassName: &storageClassName, AccessModes: []corev1.PersistentVolumeAccessMode{ "ReadWriteOnce", }, Resources: corev1.VolumeResourceRequirements{ Requests: corev1.ResourceList{ corev1.ResourceStorage: resource.MustParse("1Gi"), }, }, }, }, }, }, }) statefulSetAnalyzer := StatefulSetAnalyzer{} config := common.Analyzer{ Client: &kubernetes.Client{ Client: clientset, }, Context: context.Background(), Namespace: "default", } analysisResults, err := statefulSetAnalyzer.Analyze(config) if err != nil { t.Error(err) } var errorFound bool want := "StatefulSet uses the storage class example-sc which does not exist." for _, analysis := range analysisResults { for _, got := range analysis.Error { if want == got.Text { errorFound = true } } if errorFound { break } } if !errorFound { t.Errorf("Error expected: '%v', not found in StatefulSet's analysis results", want) } } func TestStatefulSetAnalyzerNamespaceFiltering(t *testing.T) { clientset := fake.NewSimpleClientset( &appsv1.StatefulSet{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "default", }, }, &appsv1.StatefulSet{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "other-namespace", }, }) statefulSetAnalyzer := StatefulSetAnalyzer{} config := common.Analyzer{ Client: &kubernetes.Client{ Client: clientset, }, Context: context.Background(), Namespace: "default", } analysisResults, err := statefulSetAnalyzer.Analyze(config) if err != nil { t.Error(err) } assert.Equal(t, len(analysisResults), 1) } func TestStatefulSetAnalyzerLabelSelectorFiltering(t *testing.T) { clientSet := fake.NewSimpleClientset( &appsv1.StatefulSet{ ObjectMeta: metav1.ObjectMeta{ Name: "example1", Namespace: "default", Labels: map[string]string{ "app": "statefulset", "part-of": "test", }, }, }, &appsv1.StatefulSet{ ObjectMeta: metav1.ObjectMeta{ Name: "example2", Namespace: "default", }, }, ) config := common.Analyzer{ Client: &kubernetes.Client{ Client: clientSet, }, Context: context.Background(), Namespace: "default", LabelSelector: "app=statefulset", } statefulSetAnalyzer := StatefulSetAnalyzer{} results, err := statefulSetAnalyzer.Analyze(config) if err != nil { t.Error(err) } assert.Equal(t, 1, len(results)) assert.Equal(t, "default/example1", results[0].Name) config = common.Analyzer{ Client: &kubernetes.Client{ Client: clientSet, }, Context: context.Background(), Namespace: "default", LabelSelector: "app=statefulset,part-of=test", } statefulSetAnalyzer = StatefulSetAnalyzer{} results, err = statefulSetAnalyzer.Analyze(config) if err != nil { t.Error(err) } assert.Equal(t, 1, len(results)) assert.Equal(t, "default/example1", results[0].Name) } func TestStatefulSetAnalyzerReplica(t *testing.T) { replicas := int32(3) pods := []*corev1.Pod{ { ObjectMeta: metav1.ObjectMeta{ Name: "example-0", Namespace: "default", }, Status: corev1.PodStatus{ Phase: corev1.PodRunning, }, }, { ObjectMeta: metav1.ObjectMeta{ Name: "example-1", Namespace: "default", }, Status: corev1.PodStatus{ Phase: corev1.PodRunning, }, }, { ObjectMeta: metav1.ObjectMeta{ Name: "example-2", Namespace: "default", }, Status: corev1.PodStatus{ Phase: corev1.PodRunning, }, }, } clientset := fake.NewSimpleClientset( &appsv1.StatefulSet{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "default", }, Spec: appsv1.StatefulSetSpec{ Replicas: &replicas, }, Status: appsv1.StatefulSetStatus{ AvailableReplicas: 3, }, }, pods[0], pods[1], pods[2], ) statefulSetAnalyzer := StatefulSetAnalyzer{} config := common.Analyzer{ Client: &kubernetes.Client{ Client: clientset, }, Context: context.Background(), Namespace: "default", } analysisResults, err := statefulSetAnalyzer.Analyze(config) if err != nil { t.Error(err) } assert.Equal(t, len(analysisResults), 1) } func TestStatefulSetAnalyzerUnavailableReplicas(t *testing.T) { replicas := int32(3) clientset := fake.NewSimpleClientset( &appsv1.StatefulSet{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "default", }, Spec: appsv1.StatefulSetSpec{ Replicas: &replicas, }, Status: appsv1.StatefulSetStatus{ AvailableReplicas: 0, }, }) statefulSetAnalyzer := StatefulSetAnalyzer{} config := common.Analyzer{ Client: &kubernetes.Client{ Client: clientset, }, Context: context.Background(), Namespace: "default", } analysisResults, err := statefulSetAnalyzer.Analyze(config) if err != nil { t.Error(err) } assert.Equal(t, len(analysisResults), 1) } func TestStatefulSetAnalyzerUnavailableReplicaWithPodInitialized(t *testing.T) { replicas := int32(3) pods := []*corev1.Pod{ { ObjectMeta: metav1.ObjectMeta{ Name: "example-0", Namespace: "default", }, Status: corev1.PodStatus{ Phase: corev1.PodRunning, }, }, { ObjectMeta: metav1.ObjectMeta{ Name: "example-1", Namespace: "default", }, Status: corev1.PodStatus{ Phase: corev1.PodPending, }, }, } clientset := fake.NewSimpleClientset( &appsv1.StatefulSet{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "default", }, Spec: appsv1.StatefulSetSpec{ Replicas: &replicas, }, Status: appsv1.StatefulSetStatus{ AvailableReplicas: 1, }, }, pods[0], pods[1], ) statefulSetAnalyzer := StatefulSetAnalyzer{} config := common.Analyzer{ Client: &kubernetes.Client{ Client: clientset, }, Context: context.Background(), Namespace: "default", } analysisResults, err := statefulSetAnalyzer.Analyze(config) if err != nil { t.Error(err) } var errorFound bool want := "Statefulset pod example-1 in the namespace default is not in running state." for _, analysis := range analysisResults { for _, got := range analysis.Error { if want == got.Text { errorFound = true } } if errorFound { break } } if !errorFound { t.Errorf("Error expected: '%v', not found in StatefulSet's analysis results", want) } } ================================================ FILE: pkg/analyzer/storage.go ================================================ /* Copyright 2024 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package analyzer import ( "fmt" "github.com/k8sgpt-ai/k8sgpt/pkg/common" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) type StorageAnalyzer struct{} func (StorageAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) { kind := "Storage" AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{ "analyzer_name": kind, }) var results []common.Result // Analyze StorageClasses scResults, err := analyzeStorageClasses(a) if err != nil { return nil, err } results = append(results, scResults...) // Analyze PersistentVolumes pvResults, err := analyzePersistentVolumes(a) if err != nil { return nil, err } results = append(results, pvResults...) // Analyze PVCs with enhanced checks pvcResults, err := analyzePersistentVolumeClaims(a) if err != nil { return nil, err } results = append(results, pvcResults...) return results, nil } func analyzeStorageClasses(a common.Analyzer) ([]common.Result, error) { var results []common.Result scs, err := a.Client.GetClient().StorageV1().StorageClasses().List(a.Context, metav1.ListOptions{}) if err != nil { return nil, err } for _, sc := range scs.Items { var failures []common.Failure // Check for deprecated storage classes if sc.Provisioner == "kubernetes.io/no-provisioner" { failures = append(failures, common.Failure{ Text: fmt.Sprintf("StorageClass %s uses deprecated provisioner 'kubernetes.io/no-provisioner'", sc.Name), Sensitive: []common.Sensitive{}, }) } // Check for default storage class if sc.Annotations["storageclass.kubernetes.io/is-default-class"] == "true" { // Check if there are multiple default storage classes defaultCount := 0 for _, otherSc := range scs.Items { if otherSc.Annotations["storageclass.kubernetes.io/is-default-class"] == "true" { defaultCount++ } } if defaultCount > 1 { failures = append(failures, common.Failure{ Text: fmt.Sprintf("Multiple default StorageClasses found (%d), which can cause confusion", defaultCount), Sensitive: []common.Sensitive{}, }) } } if len(failures) > 0 { results = append(results, common.Result{ Kind: "Storage/StorageClass", Name: sc.Name, Error: failures, }) AnalyzerErrorsMetric.WithLabelValues("Storage/StorageClass", sc.Name, "").Set(float64(len(failures))) } } return results, nil } func analyzePersistentVolumes(a common.Analyzer) ([]common.Result, error) { var results []common.Result pvs, err := a.Client.GetClient().CoreV1().PersistentVolumes().List(a.Context, metav1.ListOptions{}) if err != nil { return nil, err } for _, pv := range pvs.Items { var failures []common.Failure // Check for released PVs if pv.Status.Phase == v1.VolumeReleased { failures = append(failures, common.Failure{ Text: fmt.Sprintf("PersistentVolume %s is in Released state and should be cleaned up", pv.Name), Sensitive: []common.Sensitive{}, }) } // Check for failed PVs if pv.Status.Phase == v1.VolumeFailed { failures = append(failures, common.Failure{ Text: fmt.Sprintf("PersistentVolume %s is in Failed state", pv.Name), Sensitive: []common.Sensitive{}, }) } // Check for small PVs (less than 1Gi) if capacity, ok := pv.Spec.Capacity[v1.ResourceStorage]; ok { if capacity.Cmp(resource.MustParse("1Gi")) < 0 { failures = append(failures, common.Failure{ Text: fmt.Sprintf("PersistentVolume %s has small capacity (%s)", pv.Name, capacity.String()), Sensitive: []common.Sensitive{}, }) } } if len(failures) > 0 { results = append(results, common.Result{ Kind: "Storage/PersistentVolume", Name: pv.Name, Error: failures, }) AnalyzerErrorsMetric.WithLabelValues("Storage/PersistentVolume", pv.Name, "").Set(float64(len(failures))) } } return results, nil } func analyzePersistentVolumeClaims(a common.Analyzer) ([]common.Result, error) { var results []common.Result pvcs, err := a.Client.GetClient().CoreV1().PersistentVolumeClaims(a.Namespace).List(a.Context, metav1.ListOptions{ LabelSelector: a.LabelSelector, }) if err != nil { return nil, err } for _, pvc := range pvcs.Items { var failures []common.Failure // Check for PVC state issues first (most critical) switch pvc.Status.Phase { case v1.ClaimPending: failures = append(failures, common.Failure{ Text: fmt.Sprintf("PersistentVolumeClaim %s is in Pending state", pvc.Name), Sensitive: []common.Sensitive{}, }) case v1.ClaimLost: failures = append(failures, common.Failure{ Text: fmt.Sprintf("PersistentVolumeClaim %s is in Lost state", pvc.Name), Sensitive: []common.Sensitive{}, }) default: // Only check other issues if PVC is not in a critical state if capacity, ok := pvc.Spec.Resources.Requests[v1.ResourceStorage]; ok { if capacity.Cmp(resource.MustParse("1Gi")) < 0 { failures = append(failures, common.Failure{ Text: fmt.Sprintf("PersistentVolumeClaim %s has small capacity (%s)", pvc.Name, capacity.String()), Sensitive: []common.Sensitive{}, }) } } // Check for missing storage class if pvc.Spec.StorageClassName == nil && pvc.Spec.VolumeName == "" { failures = append(failures, common.Failure{ Text: fmt.Sprintf("PersistentVolumeClaim %s has no StorageClass specified", pvc.Name), Sensitive: []common.Sensitive{}, }) } } // Only report the first failure found if len(failures) > 0 { results = append(results, common.Result{ Kind: "Storage/PersistentVolumeClaim", Name: fmt.Sprintf("%s/%s", pvc.Namespace, pvc.Name), Error: failures[:1], }) AnalyzerErrorsMetric.WithLabelValues("Storage/PersistentVolumeClaim", pvc.Name, pvc.Namespace).Set(1) } } return results, nil } ================================================ FILE: pkg/analyzer/storage_test.go ================================================ /* Copyright 2024 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package analyzer import ( "context" "testing" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" v1 "k8s.io/api/core/v1" storagev1 "k8s.io/api/storage/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/fake" ) func TestStorageAnalyzer(t *testing.T) { tests := []struct { name string namespace string storageClasses []storagev1.StorageClass pvs []v1.PersistentVolume pvcs []v1.PersistentVolumeClaim expectedErrors int }{ { name: "Deprecated StorageClass", namespace: "default", storageClasses: []storagev1.StorageClass{ { ObjectMeta: metav1.ObjectMeta{ Name: "deprecated-sc", }, Provisioner: "kubernetes.io/no-provisioner", }, }, expectedErrors: 1, }, { name: "Multiple Default StorageClasses", namespace: "default", storageClasses: []storagev1.StorageClass{ { ObjectMeta: metav1.ObjectMeta{ Name: "default-sc1", Annotations: map[string]string{ "storageclass.kubernetes.io/is-default-class": "true", }, }, Provisioner: "kubernetes.io/gce-pd", }, { ObjectMeta: metav1.ObjectMeta{ Name: "default-sc2", Annotations: map[string]string{ "storageclass.kubernetes.io/is-default-class": "true", }, }, Provisioner: "kubernetes.io/aws-ebs", }, }, expectedErrors: 2, }, { name: "Released PV", namespace: "default", pvs: []v1.PersistentVolume{ { ObjectMeta: metav1.ObjectMeta{ Name: "released-pv", }, Status: v1.PersistentVolumeStatus{ Phase: v1.VolumeReleased, }, }, }, expectedErrors: 1, }, { name: "Failed PV", namespace: "default", pvs: []v1.PersistentVolume{ { ObjectMeta: metav1.ObjectMeta{ Name: "failed-pv", }, Status: v1.PersistentVolumeStatus{ Phase: v1.VolumeFailed, }, }, }, expectedErrors: 1, }, { name: "Small PV", namespace: "default", pvs: []v1.PersistentVolume{ { ObjectMeta: metav1.ObjectMeta{ Name: "small-pv", }, Spec: v1.PersistentVolumeSpec{ Capacity: v1.ResourceList{ v1.ResourceStorage: resource.MustParse("500Mi"), }, }, }, }, expectedErrors: 1, }, { name: "Pending PVC", namespace: "default", pvcs: []v1.PersistentVolumeClaim{ { ObjectMeta: metav1.ObjectMeta{ Name: "pending-pvc", Namespace: "default", }, Status: v1.PersistentVolumeClaimStatus{ Phase: v1.ClaimPending, }, }, }, expectedErrors: 1, }, { name: "Lost PVC", namespace: "default", pvcs: []v1.PersistentVolumeClaim{ { ObjectMeta: metav1.ObjectMeta{ Name: "lost-pvc", Namespace: "default", }, Status: v1.PersistentVolumeClaimStatus{ Phase: v1.ClaimLost, }, }, }, expectedErrors: 1, }, { name: "Small PVC", namespace: "default", pvcs: []v1.PersistentVolumeClaim{ { ObjectMeta: metav1.ObjectMeta{ Name: "small-pvc", Namespace: "default", }, Spec: v1.PersistentVolumeClaimSpec{ Resources: v1.VolumeResourceRequirements{ Requests: v1.ResourceList{ v1.ResourceStorage: resource.MustParse("500Mi"), }, }, }, }, }, expectedErrors: 1, }, { name: "PVC without StorageClass", namespace: "default", pvcs: []v1.PersistentVolumeClaim{ { ObjectMeta: metav1.ObjectMeta{ Name: "no-sc-pvc", Namespace: "default", }, Spec: v1.PersistentVolumeClaimSpec{ Resources: v1.VolumeResourceRequirements{ Requests: v1.ResourceList{ v1.ResourceStorage: resource.MustParse("1Gi"), }, }, }, }, }, expectedErrors: 1, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Create fake client client := fake.NewSimpleClientset() // Create test resources for _, sc := range tt.storageClasses { _, err := client.StorageV1().StorageClasses().Create(context.TODO(), &sc, metav1.CreateOptions{}) if err != nil { t.Fatalf("Failed to create StorageClass: %v", err) } } for _, pv := range tt.pvs { _, err := client.CoreV1().PersistentVolumes().Create(context.TODO(), &pv, metav1.CreateOptions{}) if err != nil { t.Fatalf("Failed to create PV: %v", err) } } for _, pvc := range tt.pvcs { _, err := client.CoreV1().PersistentVolumeClaims(tt.namespace).Create(context.TODO(), &pvc, metav1.CreateOptions{}) if err != nil { t.Fatalf("Failed to create PVC: %v", err) } } // Create analyzer analyzer := StorageAnalyzer{} // Create analyzer config config := common.Analyzer{ Client: &kubernetes.Client{ Client: client, }, Context: context.TODO(), Namespace: tt.namespace, } // Run analysis results, err := analyzer.Analyze(config) if err != nil { t.Fatalf("Failed to run analysis: %v", err) } // Count total errors totalErrors := 0 for _, result := range results { totalErrors += len(result.Error) } // Check error count if totalErrors != tt.expectedErrors { t.Errorf("Expected %d errors, got %d", tt.expectedErrors, totalErrors) } }) } } ================================================ FILE: pkg/analyzer/subscription.go ================================================ package analyzer import ( "fmt" "github.com/k8sgpt-ai/k8sgpt/pkg/common" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" ) type SubscriptionAnalyzer struct{} var subGVR = schema.GroupVersionResource{ Group: "operators.coreos.com", Version: "v1alpha1", Resource: "subscriptions", } func (SubscriptionAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) { kind := "Subscription" if a.Client.GetDynamicClient() == nil { return nil, fmt.Errorf("dynamic client is nil in %s analyzer", kind) } list, err := a.Client.GetDynamicClient(). Resource(subGVR).Namespace(metav1.NamespaceAll). List(a.Context, metav1.ListOptions{}) if err != nil { return nil, err } var results []common.Result for _, item := range list.Items { ns, name := item.GetNamespace(), item.GetName() state, _, _ := unstructured.NestedString(item.Object, "status", "state") conds, _, _ := unstructured.NestedSlice(item.Object, "status", "conditions") var failures []common.Failure if state == "" || state == "UpgradePending" || state == "UpgradeAvailable" { msg := "subscription not at latest" if c := pickWorstCondition(conds); c != "" { msg += "; " + c } failures = append(failures, common.Failure{Text: fmt.Sprintf("state=%q: %s", state, msg)}) } if len(failures) > 0 { results = append(results, common.Result{ Kind: kind, Name: ns + "/" + name, Error: failures, }) } } return results, nil } ================================================ FILE: pkg/analyzer/subscription_test.go ================================================ package analyzer import ( "context" "strings" "testing" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" dynamicfake "k8s.io/client-go/dynamic/fake" ) func TestSubscriptionAnalyzer(t *testing.T) { ok := &unstructured.Unstructured{ Object: map[string]any{ "apiVersion": "operators.coreos.com/v1alpha1", "kind": "Subscription", "metadata": map[string]any{ "name": "ok-sub", "namespace": "ns1", }, "status": map[string]any{ "state": "AtLatestKnown", }, }, } bad := &unstructured.Unstructured{ Object: map[string]any{ "apiVersion": "operators.coreos.com/v1alpha1", "kind": "Subscription", "metadata": map[string]any{ "name": "upgrade-sub", "namespace": "ns1", }, "status": map[string]any{ "state": "UpgradeAvailable", "conditions": []interface{}{ map[string]any{ "status": "False", "reason": "CatalogSourcesUnhealthy", "message": "not reachable", }, }, }, }, } listKinds := map[schema.GroupVersionResource]string{ {Group: "operators.coreos.com", Version: "v1alpha1", Resource: "subscriptions"}: "SubscriptionList", } scheme := runtime.NewScheme() dc := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(scheme, listKinds, ok, bad) a := common.Analyzer{ Context: context.TODO(), Client: &kubernetes.Client{DynamicClient: dc}, } res, err := (SubscriptionAnalyzer{}).Analyze(a) if err != nil { t.Fatalf("Analyze error: %v", err) } if len(res) != 1 { t.Fatalf("expected 1 result, got %d", len(res)) } if res[0].Kind != "Subscription" || !strings.Contains(res[0].Name, "ns1/upgrade-sub") { t.Fatalf("unexpected result: %#v", res[0]) } if len(res[0].Error) == 0 || !strings.Contains(res[0].Error[0].Text, "CatalogSourcesUnhealthy") { t.Fatalf("expected 'CatalogSourcesUnhealthy' in failure, got %#v", res[0].Error) } } ================================================ FILE: pkg/analyzer/test_utils.go ================================================ package analyzer // Helper functions for tests func boolPtr(b bool) *bool { return &b } func int64Ptr(i int64) *int64 { return &i } ================================================ FILE: pkg/analyzer/validating_webhook.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package analyzer import ( "context" "fmt" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" "github.com/k8sgpt-ai/k8sgpt/pkg/util" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" ) type ValidatingWebhookAnalyzer struct{} func (ValidatingWebhookAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) { kind := "ValidatingWebhookConfiguration" apiDoc := kubernetes.K8sApiReference{ Kind: kind, ApiVersion: schema.GroupVersion{ Group: "apps", Version: "v1", }, OpenapiSchema: a.OpenapiSchema, } AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{ "analyzer_name": kind, }) validatingWebhooks, err := a.Client.GetClient().AdmissionregistrationV1().ValidatingWebhookConfigurations().List(context.Background(), v1.ListOptions{LabelSelector: a.LabelSelector}) if err != nil { return nil, err } var preAnalysis = map[string]common.PreAnalysis{} for _, webhookConfig := range validatingWebhooks.Items { for _, webhook := range webhookConfig.Webhooks { var failures []common.Failure if webhook.ClientConfig.Service == nil { continue } svc := webhook.ClientConfig.Service // Get the service service, err := a.Client.GetClient().CoreV1().Services(svc.Namespace).Get(context.Background(), svc.Name, v1.GetOptions{}) if err != nil { // If the service is not found, we can't check the pods failures = append(failures, common.Failure{ Text: fmt.Sprintf("Service %s not found as mapped to by Validating Webhook %s", svc.Name, webhook.Name), KubernetesDoc: apiDoc.GetApiDocV2("spec.webhook.clientConfig.service"), Sensitive: []common.Sensitive{ { Unmasked: webhookConfig.Namespace, Masked: util.MaskString(webhookConfig.Namespace), }, { Unmasked: svc.Name, Masked: util.MaskString(svc.Name), }, }, }) preAnalysis[fmt.Sprintf("%s/%s", webhookConfig.Namespace, webhook.Name)] = common.PreAnalysis{ ValidatingWebhook: webhookConfig, FailureDetails: failures, } AnalyzerErrorsMetric.WithLabelValues(kind, webhook.Name, webhookConfig.Namespace).Set(float64(len(failures))) continue } // When Service selectors are empty we defer to service analyser if len(service.Spec.Selector) == 0 { continue } // Get pods within service pods, err := a.Client.GetClient().CoreV1().Pods(svc.Namespace).List(context.Background(), v1.ListOptions{ LabelSelector: util.MapToString(service.Spec.Selector), }) if err != nil { return nil, err } if len(pods.Items) == 0 { failures = append(failures, common.Failure{ Text: fmt.Sprintf("No active pods found within service %s as mapped to by Validating Webhook %s", svc.Name, webhook.Name), KubernetesDoc: apiDoc.GetApiDocV2("spec.webhook.clientConfig.service"), Sensitive: []common.Sensitive{ { Unmasked: webhookConfig.Namespace, Masked: util.MaskString(webhookConfig.Namespace), }, }, }) } for _, pod := range pods.Items { if pod.Status.Phase != "Running" { doc := apiDoc.GetApiDocV2("spec.webhook") failures = append(failures, common.Failure{ Text: fmt.Sprintf( "Validating Webhook (%s) is pointing to an inactive receiver pod (%s)", webhook.Name, pod.Name, ), KubernetesDoc: doc, Sensitive: []common.Sensitive{ { Unmasked: webhookConfig.Namespace, Masked: util.MaskString(webhookConfig.Namespace), }, { Unmasked: webhook.Name, Masked: util.MaskString(webhook.Name), }, { Unmasked: pod.Name, Masked: util.MaskString(pod.Name), }, }, }) } } if len(failures) > 0 { preAnalysis[fmt.Sprintf("%s/%s", webhookConfig.Namespace, webhook.Name)] = common.PreAnalysis{ ValidatingWebhook: webhookConfig, FailureDetails: failures, } AnalyzerErrorsMetric.WithLabelValues(kind, webhook.Name, webhookConfig.Namespace).Set(float64(len(failures))) } } } for key, value := range preAnalysis { var currentAnalysis = common.Result{ Kind: kind, Name: key, Error: value.FailureDetails, } parent, found := util.GetParent(a.Client, value.ValidatingWebhook.ObjectMeta) if found { currentAnalysis.ParentObject = parent } a.Results = append(a.Results, currentAnalysis) } return a.Results, nil } ================================================ FILE: pkg/analyzer/validating_webhook_test.go ================================================ /* Copyright 2024 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package analyzer import ( "context" "testing" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" "github.com/stretchr/testify/require" admissionregistrationv1 "k8s.io/api/admissionregistration/v1" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/fake" ) func TestValidatingWebhookAnalyzer(t *testing.T) { config := common.Analyzer{ Client: &kubernetes.Client{ Client: fake.NewSimpleClientset( &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "Pod1", Namespace: "default", Labels: map[string]string{ "pod": "Pod1", }, }, }, &v1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "test-service1", Namespace: "default", }, Spec: v1.ServiceSpec{ Selector: map[string]string{ "pod": "Pod1", }, }, }, &v1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "test-service2", Namespace: "test", }, Spec: v1.ServiceSpec{ // No such pod exists in the test namespace Selector: map[string]string{ "pod": "Pod2", }, }, }, &v1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "test-service3", Namespace: "test", }, Spec: v1.ServiceSpec{ // len(service.Spec.Selector) == 0 Selector: map[string]string{}, }, }, &admissionregistrationv1.ValidatingWebhookConfiguration{ ObjectMeta: metav1.ObjectMeta{ Name: "test-validating-webhook-config", Namespace: "test", }, Webhooks: []admissionregistrationv1.ValidatingWebhook{ { // Failure: Pointing to an inactive receiver pod Name: "webhook1", ClientConfig: admissionregistrationv1.WebhookClientConfig{ Service: &admissionregistrationv1.ServiceReference{ Name: "test-service1", Namespace: "default", }, }, }, { // Failure: No active pods found in the test namespace Name: "webhook2", ClientConfig: admissionregistrationv1.WebhookClientConfig{ Service: &admissionregistrationv1.ServiceReference{ Name: "test-service2", Namespace: "test", }, }, }, { Name: "webhook3", ClientConfig: admissionregistrationv1.WebhookClientConfig{ Service: &admissionregistrationv1.ServiceReference{ Name: "test-service3", Namespace: "test", }, }, }, { // Failure: Service doesn't exist. Name: "webhook4", ClientConfig: admissionregistrationv1.WebhookClientConfig{ Service: &admissionregistrationv1.ServiceReference{ Name: "test-service4-doesn't-exist", Namespace: "test", }, }, }, { // Service is nil. Name: "webhook5", ClientConfig: admissionregistrationv1.WebhookClientConfig{}, }, }, }, ), }, Context: context.Background(), Namespace: "default", } vwAnalyzer := ValidatingWebhookAnalyzer{} results, err := vwAnalyzer.Analyze(config) require.NoError(t, err) // The results should contain: webhook1, webhook2, and webhook4 resultsLen := 3 require.Equal(t, resultsLen, len(results)) } func TestValidatingWebhookAnalyzerLabelSelectorFiltering(t *testing.T) { clientSet := fake.NewSimpleClientset( &admissionregistrationv1.ValidatingWebhookConfiguration{ ObjectMeta: metav1.ObjectMeta{ Name: "test-validating-webhook-config1", Namespace: "default", Labels: map[string]string{ "app": "validating-webhook", "part-of": "test", }, }, Webhooks: []admissionregistrationv1.ValidatingWebhook{ { // Failure: Pointing to an inactive receiver pod Name: "webhook1", ClientConfig: admissionregistrationv1.WebhookClientConfig{ Service: &admissionregistrationv1.ServiceReference{ Name: "test-service1", Namespace: "default", }, }, }, }, }, &admissionregistrationv1.ValidatingWebhookConfiguration{ ObjectMeta: metav1.ObjectMeta{ Name: "test-validating-webhook-config2", Namespace: "default", }, Webhooks: []admissionregistrationv1.ValidatingWebhook{ { // Failure: Pointing to an inactive receiver pod Name: "webhook1", ClientConfig: admissionregistrationv1.WebhookClientConfig{ Service: &admissionregistrationv1.ServiceReference{ Name: "test-service1", Namespace: "default", }, }, }, }, }, ) config := common.Analyzer{ Client: &kubernetes.Client{ Client: clientSet, }, Context: context.Background(), Namespace: "default", LabelSelector: "app=validating-webhook", } vwAnalyzer := ValidatingWebhookAnalyzer{} results, err := vwAnalyzer.Analyze(config) if err != nil { t.Error(err) } require.Equal(t, 1, len(results)) config = common.Analyzer{ Client: &kubernetes.Client{ Client: clientSet, }, Context: context.Background(), Namespace: "default", LabelSelector: "app=validating-webhook,part-of=test", } vwAnalyzer = ValidatingWebhookAnalyzer{} results, err = vwAnalyzer.Analyze(config) if err != nil { t.Error(err) } require.Equal(t, 1, len(results)) } ================================================ FILE: pkg/cache/azuresa_based.go ================================================ package cache import ( "bytes" "context" "fmt" "log" "strings" "github.com/Azure/azure-sdk-for-go/sdk/azidentity" "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob" "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/blob" ) // Generate ICache implementation type AzureCache struct { ctx context.Context noCache bool containerName string session *azblob.Client } type AzureCacheConfiguration struct { StorageAccount string `mapstructure:"storageaccount" yaml:"storageaccount,omitempty"` ContainerName string `mapstructure:"container" yaml:"container,omitempty"` } func (s *AzureCache) Configure(cacheInfo CacheProvider) error { s.ctx = context.Background() if cacheInfo.Azure.ContainerName == "" { log.Fatal("Azure Container name not configured") } if cacheInfo.Azure.StorageAccount == "" { log.Fatal("Azure Storage account not configured") } // We assume that Storage account is already in place blobUrl := fmt.Sprintf("https://%s.blob.core.windows.net/", cacheInfo.Azure.StorageAccount) credential, err := azidentity.NewDefaultAzureCredential(nil) if err != nil { log.Fatal(err) } client, err := azblob.NewClient(blobUrl, credential, nil) if err != nil { log.Fatal(err) } // Try to create the blob container _, err = client.CreateContainer(s.ctx, cacheInfo.Azure.ContainerName, nil) if err != nil { // TODO: Maybe there is a better way to check this? // docs: https://pkg.go.dev/github.com/Azure/azure-storage-blob-go/azblob if strings.Contains(err.Error(), "ContainerAlreadyExists") { // do nothing } else { return err } } s.containerName = cacheInfo.Azure.ContainerName s.session = client return nil } func (s *AzureCache) Store(key string, data string) error { // Store the object as a new file in the Azure blob storage with data as the content cacheData := []byte(data) _, err := s.session.UploadBuffer(s.ctx, s.containerName, key, cacheData, &azblob.UploadBufferOptions{}) return err } func (s *AzureCache) Load(key string) (string, error) { // Load blob file contents load, err := s.session.DownloadStream(s.ctx, s.containerName, key, nil) if err != nil { return "", err } data := bytes.Buffer{} retryReader := load.NewRetryReader(s.ctx, &azblob.RetryReaderOptions{}) _, err = data.ReadFrom(retryReader) if err != nil { return "", err } if err := retryReader.Close(); err != nil { return "", err } return data.String(), nil } func (s *AzureCache) List() ([]CacheObjectDetails, error) { // List the files in the blob containerName files := []CacheObjectDetails{} pager := s.session.NewListBlobsFlatPager(s.containerName, &azblob.ListBlobsFlatOptions{ Include: azblob.ListBlobsInclude{Snapshots: false, Versions: false}, }) for pager.More() { resp, err := pager.NextPage(s.ctx) if err != nil { return nil, err } for _, blob := range resp.Segment.BlobItems { files = append(files, CacheObjectDetails{ Name: *blob.Name, UpdatedAt: *blob.Properties.LastModified, }) } } return files, nil } func (s *AzureCache) Remove(key string) error { _, err := s.session.DeleteBlob(s.ctx, s.containerName, key, &blob.DeleteOptions{}) if err != nil { return err } return nil } func (s *AzureCache) Exists(key string) bool { // Check if the object exists in the blob storage pager := s.session.NewListBlobsFlatPager(s.containerName, &azblob.ListBlobsFlatOptions{ Include: azblob.ListBlobsInclude{Snapshots: false, Versions: false}, }) for pager.More() { resp, err := pager.NextPage(s.ctx) if err != nil { return false } for _, blob := range resp.Segment.BlobItems { if *blob.Name == key { return true } } } return false } func (s *AzureCache) IsCacheDisabled() bool { return s.noCache } func (s *AzureCache) GetName() string { return "azure" } func (s *AzureCache) DisableCache() { s.noCache = true } ================================================ FILE: pkg/cache/cache.go ================================================ package cache import ( "fmt" "github.com/spf13/viper" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) var ( types = []ICache{ &AzureCache{}, &FileBasedCache{}, &GCSCache{}, &S3Cache{}, &InterplexCache{}, } ) type ICache interface { Configure(cacheInfo CacheProvider) error Store(key string, data string) error Load(key string) (string, error) List() ([]CacheObjectDetails, error) Remove(key string) error Exists(key string) bool IsCacheDisabled() bool GetName() string DisableCache() } func New(cacheType string) ICache { for _, t := range types { if cacheType == t.GetName() { return t } } return &FileBasedCache{} } func ParseCacheConfiguration() (CacheProvider, error) { var cacheInfo CacheProvider err := viper.UnmarshalKey("cache", &cacheInfo) if err != nil { return cacheInfo, err } return cacheInfo, nil } func NewCacheProvider(cacheType, bucketname, region, endpoint, storageAccount, containerName, projectId string, insecure bool) (CacheProvider, error) { cProvider := CacheProvider{} switch { case cacheType == "azure": cProvider.Azure.ContainerName = containerName cProvider.Azure.StorageAccount = storageAccount cProvider.CurrentCacheType = "azure" case cacheType == "gcs": cProvider.GCS.BucketName = bucketname cProvider.GCS.ProjectId = projectId cProvider.GCS.Region = region cProvider.CurrentCacheType = "gcs" case cacheType == "s3": cProvider.S3.BucketName = bucketname cProvider.S3.Region = region cProvider.S3.Endpoint = endpoint cProvider.S3.InsecureSkipVerify = insecure cProvider.CurrentCacheType = "s3" case cacheType == "interplex": cProvider.Interplex.ConnectionString = endpoint cProvider.CurrentCacheType = "interplex" default: return CacheProvider{}, status.Error(codes.Internal, fmt.Sprintf("%s is not a valid option", cacheType)) } cache := New(cacheType) err := cache.Configure(cProvider) if err != nil { return CacheProvider{}, err } return cProvider, nil } // If we have set a remote cache, return the remote cache configuration func GetCacheConfiguration() (ICache, error) { cacheInfo, err := ParseCacheConfiguration() if err != nil { return nil, err } var cache ICache switch { case cacheInfo.CurrentCacheType == "gcs": cache = &GCSCache{} case cacheInfo.CurrentCacheType == "azure": cache = &AzureCache{} case cacheInfo.CurrentCacheType == "s3": cache = &S3Cache{} case cacheInfo.CurrentCacheType == "interplex": cache = &InterplexCache{} default: cache = &FileBasedCache{} } err_config := cache.Configure(cacheInfo) return cache, err_config } func AddRemoteCache(cacheInfo CacheProvider) error { viper.Set("cache", cacheInfo) err := viper.WriteConfig() if err != nil { return err } return nil } func RemoveRemoteCache() error { var cacheInfo CacheProvider err := viper.UnmarshalKey("cache", &cacheInfo) if err != nil { return status.Error(codes.Internal, "cache unmarshal") } cacheInfo = CacheProvider{} viper.Set("cache", cacheInfo) err = viper.WriteConfig() if err != nil { return status.Error(codes.Internal, "unable to write config") } return nil } ================================================ FILE: pkg/cache/cache_test.go ================================================ package cache import ( "os" "testing" "github.com/spf13/viper" "github.com/stretchr/testify/require" ) func TestNewReturnsExpectedCache(t *testing.T) { require.IsType(t, &FileBasedCache{}, New("file")) require.IsType(t, &AzureCache{}, New("azure")) require.IsType(t, &GCSCache{}, New("gcs")) require.IsType(t, &S3Cache{}, New("s3")) require.IsType(t, &InterplexCache{}, New("interplex")) // default fallback require.IsType(t, &FileBasedCache{}, New("unknown")) } func TestNewCacheProvider_InterplexAndInvalid(t *testing.T) { // valid: interplex cp, err := NewCacheProvider("interplex", "", "", "localhost:1", "", "", "", false) require.NoError(t, err) require.Equal(t, "interplex", cp.CurrentCacheType) require.Equal(t, "localhost:1", cp.Interplex.ConnectionString) // invalid type _, err = NewCacheProvider("not-a-type", "", "", "", "", "", "", false) require.Error(t, err) } func TestAddRemoveRemoteCacheAndGet(t *testing.T) { // isolate viper with temp config file tmpFile, err := os.CreateTemp("", "k8sgpt-cache-config-*.yaml") require.NoError(t, err) defer func() { _ = os.Remove(tmpFile.Name()) }() viper.Reset() viper.SetConfigFile(tmpFile.Name()) // add interplex remote cache cp := CacheProvider{} cp.CurrentCacheType = "interplex" cp.Interplex.ConnectionString = "localhost:1" require.NoError(t, AddRemoteCache(cp)) // read back via GetCacheConfiguration c, err := GetCacheConfiguration() require.NoError(t, err) require.IsType(t, &InterplexCache{}, c) // remove remote cache require.NoError(t, RemoveRemoteCache()) // now default should be file-based c2, err := GetCacheConfiguration() require.NoError(t, err) require.IsType(t, &FileBasedCache{}, c2) } ================================================ FILE: pkg/cache/file_based.go ================================================ package cache import ( "fmt" "os" "path/filepath" "github.com/adrg/xdg" "github.com/k8sgpt-ai/k8sgpt/pkg/util" ) var _ (ICache) = (*FileBasedCache)(nil) type FileBasedCache struct { noCache bool } func (f *FileBasedCache) Configure(cacheInfo CacheProvider) error { return nil } func (f *FileBasedCache) IsCacheDisabled() bool { return f.noCache } func (*FileBasedCache) List() ([]CacheObjectDetails, error) { path, err := xdg.CacheFile("k8sgpt") if err != nil { return nil, err } files, err := os.ReadDir(path) if err != nil { return nil, err } var result []CacheObjectDetails for _, file := range files { info, err := file.Info() if err != nil { return nil, err } result = append(result, CacheObjectDetails{ Name: file.Name(), UpdatedAt: info.ModTime(), }) } return result, nil } func (*FileBasedCache) Exists(key string) bool { path, err := xdg.CacheFile(filepath.Join("k8sgpt", key)) if err != nil { fmt.Fprintln(os.Stderr, "warning: error while testing if cache key exists:", err) return false } exists, err := util.FileExists(path) if err != nil { fmt.Fprintln(os.Stderr, "warning: error while testing if cache key exists:", err) return false } return exists } func (*FileBasedCache) Load(key string) (string, error) { path, err := xdg.CacheFile(filepath.Join("k8sgpt", key)) if err != nil { return "", err } data, err := os.ReadFile(path) if err != nil { return "", err } return string(data), nil } func (*FileBasedCache) Remove(key string) error { path, err := xdg.CacheFile(filepath.Join("k8sgpt", key)) if err != nil { return err } if err := os.Remove(path); err != nil { return err } return nil } func (*FileBasedCache) Store(key string, data string) error { path, err := xdg.CacheFile(filepath.Join("k8sgpt", key)) if err != nil { return err } return os.WriteFile(path, []byte(data), 0600) } func (s *FileBasedCache) GetName() string { return "file" } func (s *FileBasedCache) DisableCache() { s.noCache = true } ================================================ FILE: pkg/cache/file_based_test.go ================================================ package cache import ( "os" "path/filepath" "testing" "github.com/adrg/xdg" "github.com/stretchr/testify/require" ) // withTempCacheHome sets XDG_CACHE_HOME to a temp dir for test isolation. func withTempCacheHome(t *testing.T) func() { t.Helper() tmp, err := os.MkdirTemp("", "k8sgpt-cache-test-*") require.NoError(t, err) old := os.Getenv("XDG_CACHE_HOME") require.NoError(t, os.Setenv("XDG_CACHE_HOME", tmp)) return func() { _ = os.Setenv("XDG_CACHE_HOME", old) _ = os.RemoveAll(tmp) } } func TestFileBasedCache_BasicOps(t *testing.T) { cleanup := withTempCacheHome(t) defer cleanup() c := &FileBasedCache{} // Configure should be a no-op require.NoError(t, c.Configure(CacheProvider{})) require.Equal(t, "file", c.GetName()) require.False(t, c.IsCacheDisabled()) c.DisableCache() require.True(t, c.IsCacheDisabled()) key := "testkey" data := "hello" // Store require.NoError(t, c.Store(key, data)) // Exists require.True(t, c.Exists(key)) // Load got, err := c.Load(key) require.NoError(t, err) require.Equal(t, data, got) // List should include our key file items, err := c.List() require.NoError(t, err) // ensure at least one item and that one matches our key found := false for _, it := range items { if it.Name == key { found = true break } } require.True(t, found) // Remove require.NoError(t, c.Remove(key)) require.False(t, c.Exists(key)) } func TestFileBasedCache_PathShape(t *testing.T) { cleanup := withTempCacheHome(t) defer cleanup() // Verify xdg.CacheFile path shape (directory and filename) p, err := xdg.CacheFile(filepath.Join("k8sgpt", "abc")) require.NoError(t, err) require.Equal(t, "abc", filepath.Base(p)) require.Contains(t, p, "k8sgpt") } ================================================ FILE: pkg/cache/gcs_based.go ================================================ package cache import ( "context" "io" "log" "cloud.google.com/go/storage" "google.golang.org/api/iterator" ) type GCSCache struct { ctx context.Context noCache bool bucketName string projectId string region string session *storage.Client } type GCSCacheConfiguration struct { ProjectId string `mapstructure:"projectid" yaml:"projectid,omitempty"` Region string `mapstructure:"region" yaml:"region,omitempty"` BucketName string `mapstructure:"bucketname" yaml:"bucketname,omitempty"` } func (s *GCSCache) Configure(cacheInfo CacheProvider) error { s.ctx = context.Background() if cacheInfo.GCS.BucketName == "" { log.Fatal("Bucket name not configured") } if cacheInfo.GCS.Region == "" { log.Fatal("Region not configured") } if cacheInfo.GCS.ProjectId == "" { log.Fatal("ProjectID not configured") } s.bucketName = cacheInfo.GCS.BucketName s.projectId = cacheInfo.GCS.ProjectId s.region = cacheInfo.GCS.Region storageClient, err := storage.NewClient(s.ctx) if err != nil { log.Fatal(err) } _, err = storageClient.Bucket(s.bucketName).Attrs(s.ctx) if err == storage.ErrBucketNotExist { err = storageClient.Bucket(s.bucketName).Create(s.ctx, s.projectId, &storage.BucketAttrs{ Location: s.region, }) if err != nil { return err } } s.session = storageClient return nil } func (s *GCSCache) Store(key string, data string) error { wc := s.session.Bucket(s.bucketName).Object(key).NewWriter(s.ctx) if _, err := wc.Write([]byte(data)); err != nil { return err } if err := wc.Close(); err != nil { return err } return nil } func (s *GCSCache) Load(key string) (string, error) { reader, err := s.session.Bucket(s.bucketName).Object(key).NewReader(s.ctx) if err != nil { return "", err } defer reader.Close() data, err := io.ReadAll(reader) if err != nil { return "", err } return string(data), nil } func (s *GCSCache) Remove(key string) error { bucketClient := s.session.Bucket(s.bucketName) obj := bucketClient.Object(key) if err := obj.Delete(s.ctx); err != nil { return err } return nil } func (s *GCSCache) List() ([]CacheObjectDetails, error) { var files []CacheObjectDetails items := s.session.Bucket(s.bucketName).Objects(s.ctx, nil) for { attrs, err := items.Next() if err == iterator.Done { break } if err != nil { return nil, err } files = append(files, CacheObjectDetails{ Name: attrs.Name, UpdatedAt: attrs.Updated, }) } return files, nil } func (s *GCSCache) Exists(key string) bool { obj := s.session.Bucket(s.bucketName).Object(key) _, err := obj.Attrs(s.ctx) return err == nil } func (s *GCSCache) IsCacheDisabled() bool { return s.noCache } func (s *GCSCache) GetName() string { return "gcs" } func (s *GCSCache) DisableCache() { s.noCache = true } ================================================ FILE: pkg/cache/interplex_based.go ================================================ package cache import ( "context" "errors" "fmt" "os" rpc "buf.build/gen/go/interplex-ai/schemas/grpc/go/protobuf/schema/v1/schemav1grpc" schemav1 "buf.build/gen/go/interplex-ai/schemas/protocolbuffers/go/protobuf/schema/v1" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" ) var _ ICache = (*InterplexCache)(nil) type InterplexCache struct { configuration InterplexCacheConfiguration client InterplexClient cacheServiceClient rpc.CacheServiceClient noCache bool } type InterplexCacheConfiguration struct { ConnectionString string `mapstructure:"connectionString" yaml:"connectionString,omitempty"` } type InterplexClient struct { } func (c *InterplexCache) Configure(cacheInfo CacheProvider) error { if cacheInfo.Interplex.ConnectionString == "" { return errors.New("connection string is required") } c.configuration.ConnectionString = cacheInfo.Interplex.ConnectionString return nil } func (c *InterplexCache) Store(key string, data string) error { if os.Getenv("INTERPLEX_LOCAL_MODE") != "" { c.configuration.ConnectionString = "localhost:8084" } conn, err := grpc.NewClient(c.configuration.ConnectionString, grpc.WithInsecure(), grpc.WithBlock()) defer conn.Close() if err != nil { return err } serviceClient := rpc.NewCacheServiceClient(conn) c.cacheServiceClient = serviceClient req := schemav1.SetRequest{ Key: key, Value: data, } _, err = c.cacheServiceClient.Set(context.Background(), &req) if err != nil { return err } return nil } func (c *InterplexCache) Load(key string) (string, error) { if os.Getenv("INTERPLEX_LOCAL_MODE") != "" { c.configuration.ConnectionString = "localhost:8084" } conn, err := grpc.NewClient(c.configuration.ConnectionString, grpc.WithInsecure(), grpc.WithBlock()) defer conn.Close() if err != nil { return "", err } serviceClient := rpc.NewCacheServiceClient(conn) c.cacheServiceClient = serviceClient req := schemav1.GetRequest{ Key: key, } resp, err := c.cacheServiceClient.Get(context.Background(), &req) if err != nil { return "", err } return resp.Value, nil } func (c *InterplexCache) List() ([]CacheObjectDetails, error) { // Not implemented for Interplex cache return []CacheObjectDetails{}, nil } func (c *InterplexCache) Remove(key string) error { if os.Getenv("INTERPLEX_LOCAL_MODE") != "" { c.configuration.ConnectionString = "localhost:8084" } conn, err := grpc.NewClient(c.configuration.ConnectionString, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { return err } defer func() { if err := conn.Close(); err != nil { // Log the error but don't return it since this is a deferred function fmt.Printf("Error closing connection: %v\n", err) } }() serviceClient := rpc.NewCacheServiceClient(conn) c.cacheServiceClient = serviceClient req := schemav1.DeleteRequest{ Key: key, } _, err = c.cacheServiceClient.Delete(context.Background(), &req) return err } func (c *InterplexCache) Exists(key string) bool { _, err := c.Load(key) return err == nil } func (c *InterplexCache) IsCacheDisabled() bool { return c.noCache } func (c *InterplexCache) GetName() string { return "interplex" } func (c *InterplexCache) DisableCache() { c.noCache = true } ================================================ FILE: pkg/cache/interplex_based_test.go ================================================ package cache import ( rpc "buf.build/gen/go/interplex-ai/schemas/grpc/go/protobuf/schema/v1/schemav1grpc" schemav1 "buf.build/gen/go/interplex-ai/schemas/protocolbuffers/go/protobuf/schema/v1" "context" "errors" "google.golang.org/grpc" "net" "testing" ) func TestInterplexCache(t *testing.T) { cache := &InterplexCache{ configuration: InterplexCacheConfiguration{ ConnectionString: "localhost:50051", }, } // Mock GRPC server setup errChan := make(chan error, 1) go func() { lis, err := net.Listen("tcp", ":50051") if err != nil { errChan <- err return } s := grpc.NewServer() rpc.RegisterCacheServiceServer(s, &mockCacheService{}) if err := s.Serve(lis); err != nil { errChan <- err return } }() // Check if server startup failed select { case err := <-errChan: if err != nil { t.Fatalf("failed to start mock server: %v", err) } default: // Server started successfully } t.Run("TestStore", func(t *testing.T) { err := cache.Store("key1", "value1") if err != nil { t.Errorf("Error storing value: %v", err) } }) t.Run("TestLoad", func(t *testing.T) { value, err := cache.Load("key1") if err != nil { t.Errorf("Error loading value: %v", err) } if value != "value1" { t.Errorf("Expected value1, got %v", value) } }) t.Run("TestExists", func(t *testing.T) { exists := cache.Exists("key1") if !exists { t.Errorf("Expected key1 to exist") } }) } type mockCacheService struct { rpc.UnimplementedCacheServiceServer data map[string]string } func (m *mockCacheService) Set(ctx context.Context, req *schemav1.SetRequest) (*schemav1.SetResponse, error) { if m.data == nil { m.data = make(map[string]string) } m.data[req.Key] = req.Value return &schemav1.SetResponse{}, nil } func (m *mockCacheService) Get(ctx context.Context, req *schemav1.GetRequest) (*schemav1.GetResponse, error) { value, exists := m.data[req.Key] if !exists { return nil, errors.New("key not found") } return &schemav1.GetResponse{Value: value}, nil } ================================================ FILE: pkg/cache/s3_based.go ================================================ package cache import ( "bytes" "crypto/tls" "errors" "net/http" "strings" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/s3" ) // Generate ICache implementation type S3Cache struct { noCache bool bucketName string session *s3.S3 } type S3CacheConfiguration struct { Region string `mapstructure:"region" yaml:"region,omitempty"` BucketName string `mapstructure:"bucketname" yaml:"bucketname,omitempty"` Endpoint string `mapstructure:"endpoint" yaml:"endpoint,omitempty"` InsecureSkipVerify bool `mapstructure:"insecure" yaml:"insecure,omitempty"` } func (s *S3Cache) Configure(cacheInfo CacheProvider) error { if cacheInfo.S3.BucketName == "" { return errors.New("bucket name not configured") } s.bucketName = cacheInfo.S3.BucketName sess, err := session.NewSessionWithOptions(session.Options{ SharedConfigState: session.SharedConfigEnable, Config: aws.Config{ Region: aws.String(cacheInfo.S3.Region), }, }) if err != nil { return errors.New("failed to create AWS session; please check your AWS credentials and configuration: " + err.Error()) } if cacheInfo.S3.Endpoint != "" { sess.Config.Endpoint = &cacheInfo.S3.Endpoint sess.Config.S3ForcePathStyle = aws.Bool(true) transport := &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: cacheInfo.S3.InsecureSkipVerify}, } customClient := &http.Client{Transport: transport} sess.Config.HTTPClient = customClient } s3Client := s3.New(sess) // Check if the bucket exists, if not create it _, err = s3Client.HeadBucket(&s3.HeadBucketInput{ Bucket: aws.String(cacheInfo.S3.BucketName), }) if err != nil { // Check for AWS credentials error if strings.Contains(err.Error(), "InvalidAccessKeyId") || strings.Contains(err.Error(), "SignatureDoesNotMatch") || strings.Contains(err.Error(), "NoCredentialProviders") { return errors.New("aws credentials are invalid or missing; please check your AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables or AWS config") } _, err = s3Client.CreateBucket(&s3.CreateBucketInput{ Bucket: aws.String(cacheInfo.S3.BucketName), }) if err != nil { return err } } s.session = s3Client return nil } func (s *S3Cache) Store(key string, data string) error { // Store the object as a new file in the bucket with data as the content _, err := s.session.PutObject(&s3.PutObjectInput{ Body: aws.ReadSeekCloser(bytes.NewReader([]byte(data))), Bucket: aws.String(s.bucketName), Key: aws.String(key), }) return err } func (s *S3Cache) Remove(key string) error { _, err := s.session.DeleteObject(&s3.DeleteObjectInput{ Bucket: &s.bucketName, Key: aws.String(key), }) if err != nil { return err } return nil } func (s *S3Cache) Load(key string) (string, error) { // Retrieve the object from the bucket and load it into a string result, err := s.session.GetObject(&s3.GetObjectInput{ Bucket: aws.String(s.bucketName), Key: aws.String(key), }) if err != nil { return "", err } buf := new(bytes.Buffer) _, err_read := buf.ReadFrom(result.Body) result.Body.Close() return buf.String(), err_read } func (s *S3Cache) List() ([]CacheObjectDetails, error) { // List the files in the bucket result, err := s.session.ListObjectsV2(&s3.ListObjectsV2Input{Bucket: aws.String(s.bucketName)}) if err != nil { return nil, err } var keys []CacheObjectDetails for _, item := range result.Contents { keys = append(keys, CacheObjectDetails{ Name: *item.Key, UpdatedAt: *item.LastModified, }) } return keys, nil } func (s *S3Cache) Exists(key string) bool { // Check if the object exists in the bucket _, err := s.session.HeadObject(&s3.HeadObjectInput{ Bucket: aws.String(s.bucketName), Key: aws.String(key), }) return err == nil } func (s *S3Cache) IsCacheDisabled() bool { return s.noCache } func (s *S3Cache) GetName() string { return "s3" } func (s *S3Cache) DisableCache() { s.noCache = true } ================================================ FILE: pkg/cache/types.go ================================================ package cache import "time" type CacheProvider struct { CurrentCacheType string `mapstructure:"currentCacheType" yaml:"currentCacheType"` GCS GCSCacheConfiguration `mapstructure:"gcs" yaml:"gcs,omitempty"` Azure AzureCacheConfiguration `mapstructure:"azure" yaml:"azure,omitempty"` S3 S3CacheConfiguration `mapstructure:"s3" yaml:"s3,omitempty"` Interplex InterplexCacheConfiguration `mapstructure:"interplex" yaml:"interplex,omitempty"` } type CacheObjectDetails struct { Name string UpdatedAt time.Time } ================================================ FILE: pkg/common/types.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package common import ( "context" "time" openapi_v2 "github.com/google/gnostic/openapiv2" "github.com/k8sgpt-ai/k8sgpt/pkg/ai" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" keda "github.com/kedacore/keda/v2/apis/keda/v1alpha1" kyverno "github.com/kyverno/policy-reporter-kyverno-plugin/pkg/crd/api/policyreport/v1alpha2" regv1 "k8s.io/api/admissionregistration/v1" appsv1 "k8s.io/api/apps/v1" autov2 "k8s.io/api/autoscaling/v2" v1 "k8s.io/api/core/v1" networkv1 "k8s.io/api/networking/v1" policyv1 "k8s.io/api/policy/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" gtwapi "sigs.k8s.io/gateway-api/apis/v1" ) type IAnalyzer interface { Analyze(analysis Analyzer) ([]Result, error) } type Analyzer struct { Client *kubernetes.Client Context context.Context Namespace string LabelSelector string AIClient ai.IAI PreAnalysis map[string]PreAnalysis Results []Result OpenapiSchema *openapi_v2.Document } type PreAnalysis struct { Pod v1.Pod FailureDetails []Failure Deployment appsv1.Deployment ReplicaSet appsv1.ReplicaSet PersistentVolumeClaim v1.PersistentVolumeClaim Endpoint v1.Endpoints Ingress networkv1.Ingress HorizontalPodAutoscalers autov2.HorizontalPodAutoscaler PodDisruptionBudget policyv1.PodDisruptionBudget StatefulSet appsv1.StatefulSet NetworkPolicy networkv1.NetworkPolicy Node v1.Node ValidatingWebhook regv1.ValidatingWebhookConfiguration MutatingWebhook regv1.MutatingWebhookConfiguration GatewayClass gtwapi.GatewayClass Gateway gtwapi.Gateway HTTPRoute gtwapi.HTTPRoute // Integrations ScaledObject keda.ScaledObject KyvernoPolicyReport kyverno.PolicyReport KyvernoClusterPolicyReport kyverno.ClusterPolicyReport Catalog ClusterCatalog Extension ClusterExtension } type Result struct { Kind string `json:"kind"` Name string `json:"name"` Error []Failure `json:"error"` Details string `json:"details"` ParentObject string `json:"parentObject"` } type AnalysisStats struct { Analyzer string `json:"analyzer"` DurationTime time.Duration `json:"durationTime"` } type Failure struct { Text string KubernetesDoc string Sensitive []Sensitive } type Sensitive struct { Unmasked string Masked string } type ( SourceType string AvailabilityMode string UpgradeConstraintPolicy string CRDUpgradeSafetyEnforcement string ) type ClusterCatalog struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata"` Spec ClusterCatalogSpec `json:"spec"` Status ClusterCatalogStatus `json:"status,omitempty"` } type ClusterCatalogList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata"` Items []ClusterCatalog `json:"items"` } type ClusterCatalogSpec struct { Source CatalogSource `json:"source"` Priority int32 `json:"priority"` AvailabilityMode AvailabilityMode `json:"availabilityMode,omitempty"` } type ClusterCatalogStatus struct { Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"` ResolvedSource *ResolvedCatalogSource `json:"resolvedSource,omitempty"` URLs *ClusterCatalogURLs `json:"urls,omitempty"` LastUnpacked *metav1.Time `json:"lastUnpacked,omitempty"` } type ClusterCatalogURLs struct { Base string `json:"base"` } type CatalogSource struct { Type SourceType `json:"type"` Image *ImageSource `json:"image,omitempty"` } type ResolvedCatalogSource struct { Type SourceType `json:"type"` Image *ResolvedImageSource `json:"image"` } type ResolvedImageSource struct { Ref string `json:"ref"` } type ImageSource struct { Ref string `json:"ref"` PollIntervalMinutes *int `json:"pollIntervalMinutes,omitempty"` } type ClusterExtension struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Spec ClusterExtensionSpec `json:"spec,omitempty"` Status ClusterExtensionStatus `json:"status,omitempty"` } type ClusterExtensionSpec struct { Namespace string `json:"namespace"` ServiceAccount ServiceAccountReference `json:"serviceAccount"` Source SourceConfig `json:"source"` Install *ClusterExtensionInstallConfig `json:"install,omitempty"` } type ClusterExtensionInstallConfig struct { Preflight *PreflightConfig `json:"preflight,omitempty"` } type PreflightConfig struct { CRDUpgradeSafety *CRDUpgradeSafetyPreflightConfig `json:"crdUpgradeSafety"` } type CRDUpgradeSafetyPreflightConfig struct { Enforcement CRDUpgradeSafetyEnforcement `json:"enforcement"` } type ServiceAccountReference struct { Name string `json:"name"` } type SourceConfig struct { SourceType string `json:"sourceType"` Catalog *CatalogFilter `json:"catalog,omitempty"` } type CatalogFilter struct { PackageName string `json:"packageName"` Version string `json:"version,omitempty"` Channels []string `json:"channels,omitempty"` Selector *metav1.LabelSelector `json:"selector,omitempty"` UpgradeConstraintPolicy UpgradeConstraintPolicy `json:"upgradeConstraintPolicy,omitempty"` } type ClusterExtensionStatus struct { Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"` Install *ClusterExtensionInstallStatus `json:"install,omitempty"` } type ClusterExtensionInstallStatus struct { Bundle BundleMetadata `json:"bundle"` } type BundleMetadata struct { Name string `json:"name"` Version string `json:"version"` } ================================================ FILE: pkg/custom/client.go ================================================ package custom import ( rpc "buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go/schema/v1/schemav1grpc" schemav1 "buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go/schema/v1" "context" "fmt" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" ) type Client struct { c *grpc.ClientConn analyzerClient rpc.CustomAnalyzerServiceClient } func NewClient(c Connection) (*Client, error) { //nolint:staticcheck // Ignoring SA1019 for compatibility reasons conn, err := grpc.Dial(fmt.Sprintf("%s:%s", c.Url, c.Port), grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { return nil, err } client := rpc.NewCustomAnalyzerServiceClient(conn) return &Client{ c: conn, analyzerClient: client, }, nil } func (cli *Client) Run() (common.Result, error) { var result common.Result req := &schemav1.RunRequest{} res, err := cli.analyzerClient.Run(context.Background(), req) if err != nil { return result, err } if res.Result != nil { // We should refactor this, because Error and Failure do not map 1:1 from K8sGPT/schema var errorsFound []common.Failure for _, e := range res.Result.Error { errorsFound = append(errorsFound, common.Failure{ Text: e.Text, // TODO: Support sensitive data }) } result.Name = res.Result.Name result.Kind = res.Result.Kind result.Details = res.Result.Details result.ParentObject = res.Result.ParentObject result.Error = errorsFound } return result, nil } ================================================ FILE: pkg/custom/client_test.go ================================================ package custom import ( "context" "testing" schemav1 "buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go/schema/v1" "github.com/stretchr/testify/require" "google.golang.org/grpc" ) // mockAnalyzerClient implements rpc.CustomAnalyzerServiceClient for testing type mockAnalyzerClient struct{ resp *schemav1.RunResponse err error } func (m *mockAnalyzerClient) Run(ctx context.Context, in *schemav1.RunRequest, opts ...grpc.CallOption) (*schemav1.RunResponse, error) { return m.resp, m.err } func TestClientRunMapsResponse(t *testing.T) { // prepare fake response resp := &schemav1.RunResponse{ Result: &schemav1.Result{ Name: "AnalyzerA", Kind: "Pod", Details: "details", ParentObject: "Deployment/foo", }, } cli := &Client{analyzerClient: &mockAnalyzerClient{resp: resp}} got, err := cli.Run() require.NoError(t, err) require.Equal(t, "AnalyzerA", got.Name) require.Equal(t, "Pod", got.Kind) require.Equal(t, "details", got.Details) require.Equal(t, "Deployment/foo", got.ParentObject) require.Len(t, got.Error, 0) } ================================================ FILE: pkg/custom/types.go ================================================ package custom type Connection struct { Url string `json:"url"` Port string `json:"port"` } type CustomAnalyzer struct { Name string `json:"name"` Connection Connection `json:"connection"` } ================================================ FILE: pkg/custom_analyzer/customAnalyzer.go ================================================ package custom_analyzer import ( "fmt" "reflect" "regexp" ) type CustomAnalyzerConfiguration struct { Name string `mapstructure:"name"` Connection Connection `mapstructure:"connection"` } type Connection struct { Url string `mapstructure:"url"` Port int `mapstructure:"port"` } type CustomAnalyzer struct{} func NewCustomAnalyzer() *CustomAnalyzer { return &CustomAnalyzer{} } func (*CustomAnalyzer) Check(actualConfig []CustomAnalyzerConfiguration, name, url string, port int) error { validNameRegex := `^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$` validName := regexp.MustCompile(validNameRegex) if !validName.MatchString(name) { return fmt.Errorf("invalid name format. Must match %s", validNameRegex) } for _, analyzer := range actualConfig { if analyzer.Name == name { return fmt.Errorf("custom analyzer with the name '%s' already exists. Please use a different name", name) } if reflect.DeepEqual(analyzer.Connection, Connection{ Url: url, Port: port, }) { 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) } } return nil } ================================================ FILE: pkg/integration/aws/aws.go ================================================ package aws import ( "os" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/spf13/viper" ) type AWS struct { sess *session.Session } func (a *AWS) Deploy(namespace string) error { return nil } func (a *AWS) UnDeploy(namespace string) error { a.sess = nil return nil } func (a *AWS) AddAnalyzer(mergedMap *map[string]common.IAnalyzer) { // Retrieve AWS credentials from the environment accessKeyID := os.Getenv("AWS_ACCESS_KEY_ID") secretAccessKey := os.Getenv("AWS_SECRET_ACCESS_KEY") awsProfile := os.Getenv("AWS_PROFILE") var sess *session.Session if accessKeyID != "" && secretAccessKey != "" { // Use access keys if both are provided sess = session.Must(session.NewSessionWithOptions(session.Options{ Config: aws.Config{}, })) } else { // Use AWS profile, default to "default" if not set if awsProfile == "" { awsProfile = "default" } sess = session.Must(session.NewSessionWithOptions(session.Options{ Profile: awsProfile, SharedConfigState: session.SharedConfigEnable, })) } a.sess = sess (*mergedMap)["EKS"] = &EKSAnalyzer{ session: a.sess, } } func (a *AWS) GetAnalyzerName() []string { return []string{"EKS"} } func (a *AWS) GetNamespace() (string, error) { return "", nil } func (a *AWS) OwnsAnalyzer(s string) bool { for _, az := range a.GetAnalyzerName() { if s == az { return true } } return false } func (a *AWS) isFilterActive() bool { activeFilters := viper.GetStringSlice("active_filters") for _, filter := range a.GetAnalyzerName() { for _, af := range activeFilters { if af == filter { return true } } } return false } func (a *AWS) IsActivate() bool { if a.isFilterActive() { return true } else { return false } } func NewAWS() *AWS { return &AWS{} } ================================================ FILE: pkg/integration/aws/eks.go ================================================ package aws import ( "errors" "github.com/spf13/viper" "os" "path/filepath" "strings" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/eks" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "k8s.io/client-go/tools/clientcmd" ) type EKSAnalyzer struct { session *session.Session } func (e *EKSAnalyzer) Analyze(analysis common.Analyzer) ([]common.Result, error) { var cr []common.Result = []common.Result{} _ = map[string]common.PreAnalysis{} svc := eks.New(e.session) // Get the name of the current cluster var kubeconfig string kubeconfigFromPath := viper.GetString("kubeconfig") if kubeconfigFromPath != "" { kubeconfig = kubeconfigFromPath } else { kubeconfig = filepath.Join(os.Getenv("HOME"), ".kube", "config") } config, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig( &clientcmd.ClientConfigLoadingRules{ExplicitPath: kubeconfig}, &clientcmd.ConfigOverrides{ CurrentContext: "", }).RawConfig() if err != nil { return cr, err } currentConfig := config.CurrentContext if !strings.Contains(currentConfig, "eks") { return cr, errors.New("EKS cluster was not detected") } input := &eks.ListClustersInput{} result, err := svc.ListClusters(input) if err != nil { return cr, err } for _, cluster := range result.Clusters { // describe the cluster if !strings.Contains(currentConfig, *cluster) { continue } input := &eks.DescribeClusterInput{ Name: cluster, } result, err := svc.DescribeCluster(input) if err != nil { return cr, err } if len(result.Cluster.Health.Issues) > 0 { for _, issue := range result.Cluster.Health.Issues { err := make([]common.Failure, 0) err = append(err, common.Failure{ Text: issue.String(), KubernetesDoc: "", Sensitive: nil, }) cr = append(cr, common.Result{ Kind: "EKS", Name: "AWS/EKS", Error: err, }) } } } return cr, nil } ================================================ FILE: pkg/integration/integration.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package integration import ( "errors" "fmt" "github.com/k8sgpt-ai/k8sgpt/pkg/integration/aws" "github.com/k8sgpt-ai/k8sgpt/pkg/integration/kyverno" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/integration/keda" "github.com/k8sgpt-ai/k8sgpt/pkg/integration/prometheus" "github.com/k8sgpt-ai/k8sgpt/pkg/util" "github.com/spf13/viper" ) type IIntegration interface { // Add adds an integration to the cluster Deploy(namespace string) error // Remove removes an integration from the cluster UnDeploy(namespace string) error // AddAnalyzer(*map[string]common.IAnalyzer) GetAnalyzerName() []string // An integration must keep record of its deployed namespace (if not using --no-install) GetNamespace() (string, error) OwnsAnalyzer(string) bool IsActivate() bool } type Integration struct { } var integrations = map[string]IIntegration{ "prometheus": prometheus.NewPrometheus(), "aws": aws.NewAWS(), "keda": keda.NewKeda(), "kyverno": kyverno.NewKyverno(), } func NewIntegration() *Integration { return &Integration{} } func (*Integration) List() []string { keys := make([]string, 0, len(integrations)) for k := range integrations { keys = append(keys, k) } return keys } func (*Integration) Get(name string) (IIntegration, error) { if _, ok := integrations[name]; !ok { return nil, errors.New("integration not found") } return integrations[name], nil } func (i *Integration) AnalyzerByIntegration(input string) (string, error) { for _, name := range i.List() { if integ, err := i.Get(name); err == nil { if integ.OwnsAnalyzer(input) { return name, nil } } } return "", errors.New("analyzerbyintegration: no matches found") } func (*Integration) Activate(name string, namespace string, activeFilters []string, skipInstall bool) error { if _, ok := integrations[name]; !ok { return errors.New("integration not found") } if !skipInstall { if err := integrations[name].Deploy(namespace); err != nil { return fmt.Errorf("failed to deploy %s integration: %w", name, err) } } mergedFilters := activeFilters mergedFilters = append(mergedFilters, integrations[name].GetAnalyzerName()...) uniqueFilters, _ := util.RemoveDuplicates(mergedFilters) viper.Set("active_filters", uniqueFilters) if err := viper.WriteConfig(); err != nil { return fmt.Errorf("error writing config file: %s", err.Error()) } return nil } func (*Integration) Deactivate(name string, namespace string) error { if _, ok := integrations[name]; !ok { return errors.New("integration not found") } activeFilters := viper.GetStringSlice("active_filters") // Update filters and remove the specific filters for the integration for _, filter := range integrations[name].GetAnalyzerName() { for x, af := range activeFilters { if af == filter { activeFilters = append(activeFilters[:x], activeFilters[x+1:]...) } } } if err := integrations[name].UnDeploy(namespace); err != nil { return fmt.Errorf("failed to undeploy %s integration: %w", name, err) } viper.Set("active_filters", activeFilters) if err := viper.WriteConfig(); err != nil { return fmt.Errorf("error writing config file: %s", err.Error()) } return nil } func (*Integration) IsActivate(name string) (bool, error) { if _, ok := integrations[name]; !ok { return false, errors.New("integration not found") } return integrations[name].IsActivate(), nil } ================================================ FILE: pkg/integration/integration_test.go ================================================ /* Copyright 2024 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package integration import ( "os" "testing" "github.com/spf13/viper" "github.com/stretchr/testify/require" ) func TestAnalyzerByIntegration(t *testing.T) { integration := NewIntegration() _, err := integration.Get("invalid-name") require.ErrorContains(t, err, "integration not found") tests := []struct { name string expectedName string expectedErr string }{ { name: "random", expectedErr: "analyzerbyintegration: no matches found", }, { name: "PrometheusConfigValidate", expectedName: "prometheus", }, { name: "PrometheusConfigRelabelReport", expectedName: "prometheus", }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { name, err := integration.AnalyzerByIntegration(tt.name) if tt.expectedErr == "" { require.NoError(t, err) require.Equal(t, tt.expectedName, name) } else { require.ErrorContains(t, err, tt.expectedErr) require.Empty(t, name) } }) } } func TestActivate(t *testing.T) { integration := NewIntegration() err := integration.Activate("prometheus", "", []string{}, true) require.ErrorContains(t, err, "error writing config file:") err = integration.Deactivate("prometheus", "") require.ErrorContains(t, err, "error writing config file:") configFileName := "config.json" _, err = os.CreateTemp("", configFileName) require.NoError(t, err) defer os.Remove(configFileName) // Set the configuration file in viper viper.SetConfigType("json") viper.SetConfigFile(configFileName) inteNotFoundErr := "integration not found" tests := []struct { name string namespace string activeFilters []string skipInstall bool expectedIsActivate bool expectedActivationErr string expectedIsActivateError string expectedDeactivationErr string }{ { name: "invalid integration", expectedActivationErr: inteNotFoundErr, expectedIsActivateError: inteNotFoundErr, expectedDeactivationErr: inteNotFoundErr, }, { name: "prometheus", skipInstall: true, expectedIsActivate: true, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { err := integration.Activate(tt.name, tt.namespace, tt.activeFilters, tt.skipInstall) if tt.expectedActivationErr == "" { require.NoError(t, err) } else { require.ErrorContains(t, err, tt.expectedActivationErr) } ok, err := integration.IsActivate(tt.name) if tt.expectedIsActivateError == "" { require.NoError(t, err) require.Equal(t, tt.expectedIsActivate, ok) } else { require.ErrorContains(t, err, tt.expectedIsActivateError) } err = integration.Deactivate(tt.name, tt.namespace) if tt.expectedDeactivationErr == "" { require.NoError(t, err) } else { require.ErrorContains(t, err, tt.expectedDeactivationErr) } }) } } ================================================ FILE: pkg/integration/keda/keda.go ================================================ package keda import ( "context" "fmt" "os" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/fatih/color" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" "github.com/kedacore/keda/v2/pkg/generated/clientset/versioned/typed/keda/v1alpha1" helmclient "github.com/mittwald/go-helm-client" "github.com/spf13/viper" "helm.sh/helm/v3/pkg/repo" ) var ( Repo = getEnv("KEDA_REPO", "https://kedacore.github.io/charts") Version = getEnv("KEDA_VERSION", "2.11.2") ChartName = getEnv("KEDA_CHART_NAME", "keda") RepoShortName = getEnv("KEDA_REPO_SHORT_NAME", "keda") ReleaseName = getEnv("KEDA_RELEASE_NAME", "keda-k8sgpt") ) type Keda struct { helm helmclient.Client } func getEnv(key, defaultValue string) string { value := os.Getenv(key) if value == "" { return defaultValue } return value } func NewKeda() *Keda { helmClient, err := helmclient.New(&helmclient.Options{}) if err != nil { panic(err) } return &Keda{ helm: helmClient, } } func (k *Keda) Deploy(namespace string) error { // Add the repository chartRepo := repo.Entry{ Name: RepoShortName, URL: Repo, } // Add a chart-repository to the client. if err := k.helm.AddOrUpdateChartRepo(chartRepo); err != nil { panic(err) } chartSpec := helmclient.ChartSpec{ ReleaseName: ReleaseName, ChartName: fmt.Sprintf("%s/%s", RepoShortName, ChartName), Namespace: namespace, //TODO: All of this should be configurable UpgradeCRDs: true, Wait: false, Timeout: 300, CreateNamespace: true, } // Install a chart release. // Note that helmclient.Options.Namespace should ideally match the namespace in chartSpec.Namespace. if _, err := k.helm.InstallOrUpgradeChart(context.Background(), &chartSpec, nil); err != nil { return err } return nil } func (k *Keda) UnDeploy(namespace string) error { kubecontext := viper.GetString("kubecontext") kubeconfig := viper.GetString("kubeconfig") client, err := kubernetes.NewClient(kubecontext, kubeconfig) if err != nil { // TODO: better error handling color.Red("Error initialising kubernetes client: %v", err) os.Exit(1) } kedaNamespace, _ := k.GetNamespace() color.Blue(fmt.Sprintf("Keda namespace: %s\n", kedaNamespace)) kClient, _ := v1alpha1.NewForConfig(client.Config) scaledObjectList, _ := kClient.ScaledObjects("").List(context.Background(), v1.ListOptions{}) scaledJobList, _ := kClient.ScaledJobs("").List(context.Background(), v1.ListOptions{}) triggerAuthenticationList, _ := kClient.TriggerAuthentications("").List(context.Background(), v1.ListOptions{}) clusterTriggerAuthenticationsList, _ := kClient.ClusterTriggerAuthentications().List(context.Background(), v1.ListOptions{}) // Before uninstalling the Helm chart, we need to delete Keda resources for _, scaledObject := range scaledObjectList.Items { err := kClient.ScaledObjects(scaledObject.Namespace).Delete(context.Background(), scaledObject.Name, v1.DeleteOptions{}) if err != nil { fmt.Printf("Error deleting scaledObject %s: %v\n", scaledObject.Name, err) } else { fmt.Printf("Deleted scaledObject %s in namespace %s\n", scaledObject.Name, scaledObject.Namespace) } } for _, scaledJob := range scaledJobList.Items { err := kClient.ScaledJobs(scaledJob.Namespace).Delete(context.Background(), scaledJob.Name, v1.DeleteOptions{}) if err != nil { fmt.Printf("Error deleting scaledJob %s: %v\n", scaledJob.Name, err) } else { fmt.Printf("Deleted scaledJob %s in namespace %s\n", scaledJob.Name, scaledJob.Namespace) } } for _, triggerAuthentication := range triggerAuthenticationList.Items { err := kClient.TriggerAuthentications(triggerAuthentication.Namespace).Delete(context.Background(), triggerAuthentication.Name, v1.DeleteOptions{}) if err != nil { fmt.Printf("Error deleting triggerAuthentication %s: %v\n", triggerAuthentication.Name, err) } else { fmt.Printf("Deleted triggerAuthentication %s in namespace %s\n", triggerAuthentication.Name, triggerAuthentication.Namespace) } } for _, clusterTriggerAuthentication := range clusterTriggerAuthenticationsList.Items { err := kClient.ClusterTriggerAuthentications().Delete(context.Background(), clusterTriggerAuthentication.Name, v1.DeleteOptions{}) if err != nil { fmt.Printf("Error deleting clusterTriggerAuthentication %s: %v\n", clusterTriggerAuthentication.Name, err) } else { fmt.Printf("Deleted clusterTriggerAuthentication %s\n", clusterTriggerAuthentication.Name) } } chartSpec := helmclient.ChartSpec{ ReleaseName: ReleaseName, ChartName: fmt.Sprintf("%s/%s", RepoShortName, ChartName), Namespace: namespace, UpgradeCRDs: true, Wait: false, Timeout: 300, } // Uninstall the chart release. // Note that helmclient.Options.Namespace should ideally match the namespace in chartSpec.Namespace. if err := k.helm.UninstallRelease(&chartSpec); err != nil { return err } return nil } func (k *Keda) AddAnalyzer(mergedMap *map[string]common.IAnalyzer) { (*mergedMap)["ScaledObject"] = &ScaledObjectAnalyzer{} } func (k *Keda) GetAnalyzerName() []string { return []string{ "ScaledObject", } } func (k *Keda) GetNamespace() (string, error) { releases, err := k.helm.ListDeployedReleases() if err != nil { return "", err } for _, rel := range releases { if rel.Name == ReleaseName { return rel.Namespace, nil } } return "", status.Error(codes.NotFound, "keda release not found") } func (k *Keda) OwnsAnalyzer(analyzer string) bool { for _, a := range k.GetAnalyzerName() { if analyzer == a { return true } } return false } func (k *Keda) isFilterActive() bool { activeFilters := viper.GetStringSlice("active_filters") for _, filter := range k.GetAnalyzerName() { for _, af := range activeFilters { if af == filter { return true } } } return false } func (k *Keda) isDeployed() bool { kubecontext := viper.GetString("kubecontext") kubeconfig := viper.GetString("kubeconfig") client, err := kubernetes.NewClient(kubecontext, kubeconfig) if err != nil { // TODO: better error handling color.Red("Error initialising kubernetes client: %v", err) os.Exit(1) } groups, _, err := client.Client.Discovery().ServerGroupsAndResources() if err != nil { // TODO: better error handling color.Red("Error initialising discovery client: %v", err) os.Exit(1) } for _, group := range groups { if group.Name == "keda.sh" { return true } } return false } func (k *Keda) IsActivate() bool { return k.isFilterActive() && k.isDeployed() } ================================================ FILE: pkg/integration/keda/scaledobject_analyzer.go ================================================ package keda import ( "fmt" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" "github.com/k8sgpt-ai/k8sgpt/pkg/util" kedaSchema "github.com/kedacore/keda/v2/apis/keda/v1alpha1" "github.com/kedacore/keda/v2/pkg/generated/clientset/versioned/typed/keda/v1alpha1" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) type ScaledObjectAnalyzer struct{} func (s *ScaledObjectAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) { kClient, _ := v1alpha1.NewForConfig(a.Client.GetConfig()) kind := "ScaledObject" apiDoc := kubernetes.K8sApiReference{ Kind: kind, ApiVersion: kedaSchema.GroupVersion, OpenapiSchema: a.OpenapiSchema, } list, err := kClient.ScaledObjects(a.Namespace).List(a.Context, metav1.ListOptions{}) if err != nil { return nil, err } var preAnalysis = map[string]common.PreAnalysis{} for _, so := range list.Items { var failures []common.Failure scaleTargetRef := so.Spec.ScaleTargetRef if scaleTargetRef.Kind == "" { scaleTargetRef.Kind = "Deployment" } var podInfo PodInfo switch scaleTargetRef.Kind { case "Deployment": deployment, err := a.Client.GetClient().AppsV1().Deployments(so.Namespace).Get(a.Context, scaleTargetRef.Name, metav1.GetOptions{}) if err == nil { podInfo = DeploymentInfo{deployment} } case "ReplicationController": rc, err := a.Client.GetClient().CoreV1().ReplicationControllers(so.Namespace).Get(a.Context, scaleTargetRef.Name, metav1.GetOptions{}) if err == nil { podInfo = ReplicationControllerInfo{rc} } case "ReplicaSet": rs, err := a.Client.GetClient().AppsV1().ReplicaSets(so.Namespace).Get(a.Context, scaleTargetRef.Name, metav1.GetOptions{}) if err == nil { podInfo = ReplicaSetInfo{rs} } case "StatefulSet": ss, err := a.Client.GetClient().AppsV1().StatefulSets(so.Namespace).Get(a.Context, scaleTargetRef.Name, metav1.GetOptions{}) if err == nil { podInfo = StatefulSetInfo{ss} } default: failures = append(failures, common.Failure{ Text: fmt.Sprintf("ScaledObject uses %s as ScaleTargetRef which is not an option.", scaleTargetRef.Kind), Sensitive: []common.Sensitive{}, }) } if podInfo == nil { doc := apiDoc.GetApiDocV2("spec.scaleTargetRef") failures = append(failures, common.Failure{ Text: fmt.Sprintf("ScaledObject uses %s/%s as ScaleTargetRef which does not exist.", scaleTargetRef.Kind, scaleTargetRef.Name), KubernetesDoc: doc, Sensitive: []common.Sensitive{ { Unmasked: scaleTargetRef.Name, Masked: util.MaskString(scaleTargetRef.Name), }, }, }) } else { containers := len(podInfo.GetPodSpec().Containers) for _, container := range podInfo.GetPodSpec().Containers { for _, trigger := range so.Spec.Triggers { if trigger.Type == "cpu" || trigger.Type == "memory" { if container.Resources.Requests == nil || container.Resources.Limits == nil { containers-- break } } } } if containers <= 0 { doc := apiDoc.GetApiDocV2("spec.scaleTargetRef.kind") failures = append(failures, common.Failure{ Text: fmt.Sprintf("%s %s/%s does not have resource configured.", scaleTargetRef.Kind, so.Namespace, scaleTargetRef.Name), KubernetesDoc: doc, Sensitive: []common.Sensitive{ { Unmasked: scaleTargetRef.Name, Masked: util.MaskString(scaleTargetRef.Name), }, }, }) } evt, err := util.FetchLatestEvent(a.Context, a.Client, so.Namespace, so.Name) if err != nil || evt == nil { continue } if evt.Type != "Normal" { failures = append(failures, common.Failure{ Text: evt.Message, Sensitive: []common.Sensitive{ { Unmasked: scaleTargetRef.Name, Masked: util.MaskString(scaleTargetRef.Name), }, }, }) } } if len(failures) > 0 { preAnalysis[fmt.Sprintf("%s/%s", so.Namespace, so.Name)] = common.PreAnalysis{ ScaledObject: so, FailureDetails: failures, } } } for key, value := range preAnalysis { var currentAnalysis = common.Result{ Kind: kind, Name: key, Error: value.FailureDetails, } parent, _ := util.GetParent(a.Client, value.ScaledObject.ObjectMeta) currentAnalysis.ParentObject = parent a.Results = append(a.Results, currentAnalysis) } return a.Results, nil } type PodInfo interface { GetPodSpec() corev1.PodSpec } type DeploymentInfo struct { *appsv1.Deployment } func (d DeploymentInfo) GetPodSpec() corev1.PodSpec { return d.Spec.Template.Spec } // define a structure for ReplicationController type ReplicationControllerInfo struct { *corev1.ReplicationController } func (rc ReplicationControllerInfo) GetPodSpec() corev1.PodSpec { return rc.Spec.Template.Spec } // define a structure for ReplicaSet type ReplicaSetInfo struct { *appsv1.ReplicaSet } func (rs ReplicaSetInfo) GetPodSpec() corev1.PodSpec { return rs.Spec.Template.Spec } // define a structure for StatefulSet type StatefulSetInfo struct { *appsv1.StatefulSet } // implement PodInfo for StatefulSetInfo func (ss StatefulSetInfo) GetPodSpec() corev1.PodSpec { return ss.Spec.Template.Spec } ================================================ FILE: pkg/integration/kyverno/analyzer.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package kyverno import ( "fmt" ctrl "sigs.k8s.io/controller-runtime/pkg/client" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/util" "github.com/kyverno/policy-reporter-kyverno-plugin/pkg/crd/api/policyreport/v1alpha2" ) // "github.com/kyverno/policy-reporter-kyverno-plugin/pkg/crd/api/policyreport/v1alpha2" type KyvernoAnalyzer struct { policyReportAnalysis bool clusterReportAnalysis bool } func (KyvernoAnalyzer) analyzePolicyReports(a common.Analyzer) ([]common.Result, error) { result := &v1alpha2.PolicyReportList{} client := a.Client.CtrlClient err := v1alpha2.AddToScheme(client.Scheme()) if err != nil { return nil, err } if err := client.List(a.Context, result, &ctrl.ListOptions{Namespace: a.Namespace}); err != nil { return nil, err } // Find criticals and get CVE var preAnalysis = map[string]common.PreAnalysis{} for _, report := range result.Items { // For each pod there may be multiple vulnerabilities var failures []common.Failure for _, vuln := range report.Results { if vuln.Result == "fail" { // get the vulnerability ID // get the vulnerability description failures = append(failures, common.Failure{ Text: fmt.Sprintf("policy failure: %s (message: %s)", vuln.Policy, vuln.Message), Sensitive: []common.Sensitive{}, }) } } if len(failures) > 0 { preAnalysis[fmt.Sprintf("%s/%s", report.Namespace, report.Name)] = common.PreAnalysis{ KyvernoPolicyReport: report, FailureDetails: failures, } } } for key, value := range preAnalysis { var currentAnalysis = common.Result{ Kind: "PolicyReport", Name: key, Error: value.FailureDetails, } parent, _ := util.GetParent(a.Client, value.KyvernoPolicyReport.ObjectMeta) currentAnalysis.ParentObject = parent a.Results = append(a.Results, currentAnalysis) } return a.Results, nil } func (t KyvernoAnalyzer) analyzeClusterPolicyReports(a common.Analyzer) ([]common.Result, error) { result := &v1alpha2.ClusterPolicyReportList{} client := a.Client.CtrlClient err := v1alpha2.AddToScheme(client.Scheme()) if err != nil { return nil, err } if err := client.List(a.Context, result, &ctrl.ListOptions{}); err != nil { return nil, err } // Find criticals and get CVE var preAnalysis = map[string]common.PreAnalysis{} for _, report := range result.Items { // For each pod there may be multiple vulnerabilities var failures []common.Failure for _, vuln := range report.Results { if vuln.Severity == "CRITICAL" { // get the vulnerability ID // get the vulnerability description failures = append(failures, common.Failure{ Text: fmt.Sprintf("critical Vulnerability found ID: %s (learn more at: %s)", vuln.ID, vuln.Source), Sensitive: []common.Sensitive{}, }) } } if len(failures) > 0 { preAnalysis[fmt.Sprintf("%s/%s", report.Namespace, report.Name)] = common.PreAnalysis{ KyvernoClusterPolicyReport: report, FailureDetails: failures, } } } for key, value := range preAnalysis { var currentAnalysis = common.Result{ Kind: "ClusterPolicyReport", Name: key, Error: value.FailureDetails, } parent, _ := util.GetParent(a.Client, value.KyvernoClusterPolicyReport.ObjectMeta) currentAnalysis.ParentObject = parent a.Results = append(a.Results, currentAnalysis) } return a.Results, nil } func (t KyvernoAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) { if t.policyReportAnalysis { common := make([]common.Result, 0) vresult, err := t.analyzePolicyReports(a) if err != nil { return nil, err } common = append(common, vresult...) return common, nil } if t.clusterReportAnalysis { common := make([]common.Result, 0) cresult, err := t.analyzeClusterPolicyReports(a) if err != nil { return nil, err } common = append(common, cresult...) return common, nil } return make([]common.Result, 0), nil } ================================================ FILE: pkg/integration/kyverno/analyzer_test.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package kyverno import ( "context" "testing" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" "github.com/kyverno/policy-reporter-kyverno-plugin/pkg/crd/api/policyreport/v1alpha2" "github.com/stretchr/testify/assert" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" ) func buildFakeClient(t *testing.T) client.Client { objects := []client.Object{ &v1alpha2.PolicyReport{ ObjectMeta: metav1.ObjectMeta{ Name: "policy-1", Namespace: "test-ns", }, Results: []v1alpha2.PolicyReportResult{ { Category: "Other", Message: "validation failure: Images built more than 6 months ago are prohibited.", Policy: "block-stale-images", Result: "fail", }, }, }, &v1alpha2.PolicyReport{ ObjectMeta: metav1.ObjectMeta{ Name: "policy-2", Namespace: "other-ns", }, Results: []v1alpha2.PolicyReportResult{ { Category: "Other", Message: "validation failure: Images built more than 6 months ago are prohibited.", Policy: "block-stale-images", Result: "fail", }, }, }, } scheme := runtime.NewScheme() err := v1alpha2.AddToScheme(scheme) if err != nil { t.Error(err) } return fake.NewClientBuilder().WithScheme(scheme).WithObjects(objects...).Build() } func TestAnalyzerNamespaceFiltering(t *testing.T) { config := common.Analyzer{ Client: &kubernetes.Client{ CtrlClient: buildFakeClient(t), }, Context: context.Background(), Namespace: "test-ns", } // Create and run analyzer analyzer := KyvernoAnalyzer{ policyReportAnalysis: true, } results, err := analyzer.Analyze(config) if err != nil { t.Error(err) } // Verify results assert.Equal(t, len(results), 1) assert.Equal(t, results[0].Kind, "PolicyReport") assert.Equal(t, results[0].Name, "test-ns/policy-1") } func TestAnalyzerAllNamespace(t *testing.T) { config := common.Analyzer{ Client: &kubernetes.Client{ CtrlClient: buildFakeClient(t), }, Context: context.Background(), } // Create and run analyzer analyzer := KyvernoAnalyzer{ policyReportAnalysis: true, } results, err := analyzer.Analyze(config) if err != nil { t.Error(err) } // Verify results assert.Equal(t, len(results), 2) } ================================================ FILE: pkg/integration/kyverno/kyverno.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package kyverno import ( "os" "github.com/fatih/color" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" "github.com/spf13/viper" ) type Kyverno struct{} func NewKyverno() *Kyverno { return &Kyverno{} } func (k *Kyverno) GetAnalyzerName() []string { return []string{ //from wgpolicyk8s.io/v1alpha2 "PolicyReport", "ClusterPolicyReport", } } func (k *Kyverno) OwnsAnalyzer(analyzer string) bool { for _, a := range k.GetAnalyzerName() { if analyzer == a { return true } } return false } func (k *Kyverno) isDeployed() bool { // check if wgpolicyk8s apigroup is available as a marker if new policy resource available is installed on the cluster kubecontext := viper.GetString("kubecontext") kubeconfig := viper.GetString("kubeconfig") client, err := kubernetes.NewClient(kubecontext, kubeconfig) if err != nil { // TODO: better error handling color.Red("Error initialising kubernetes client: %v", err) os.Exit(1) } groups, _, err := client.Client.Discovery().ServerGroupsAndResources() if err != nil { // TODO: better error handling color.Red("Error initialising discovery client: %v", err) os.Exit(1) } for _, group := range groups { if group.Name == "kyverno.io" { return true } } return false } func (k *Kyverno) isFilterActive() bool { activeFilters := viper.GetStringSlice("active_filters") for _, filter := range k.GetAnalyzerName() { for _, af := range activeFilters { if af == filter { return true } } } return false } func (k *Kyverno) IsActivate() bool { if k.isFilterActive() && k.isDeployed() { return true } else { return false } } func (k *Kyverno) AddAnalyzer(mergedMap *map[string]common.IAnalyzer) { (*mergedMap)["PolicyReport"] = &KyvernoAnalyzer{ policyReportAnalysis: true, } (*mergedMap)["ClusterPolicyReport"] = &KyvernoAnalyzer{ clusterReportAnalysis: true, } } func (k *Kyverno) Deploy(namespace string) error { return nil } func (k *Kyverno) UnDeploy(_ string) error { return nil } func (t *Kyverno) GetNamespace() (string, error) { return "", nil } ================================================ FILE: pkg/integration/prometheus/config_analyzer.go ================================================ package prometheus import ( "bytes" "compress/gzip" "context" "errors" "fmt" "io" "net/http" "path/filepath" "strings" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/util" promconfig "github.com/prometheus/prometheus/config" yaml "gopkg.in/yaml.v2" corev1 "k8s.io/api/core/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" ) const ( prometheusContainerName = "prometheus" configReloaderContainerName = "config-reloader" prometheusConfigFlag = "--config.file=" configReloaderConfigFlag = "--config-file=" ) var prometheusPodLabels = map[string]string{ "app": "prometheus", "app.kubernetes.io/name": "prometheus", } type ConfigAnalyzer struct { } // podConfig groups a specific pod with the Prometheus configuration and any // other state used for informing the common.Result. type podConfig struct { b []byte pod *corev1.Pod } func (c *ConfigAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) { ctx := a.Context client := a.Client.GetClient() namespace := a.Namespace kind := ConfigValidate podConfigs, err := findPrometheusPodConfigs(ctx, client, namespace) if err != nil { return nil, err } var preAnalysis = map[string]common.PreAnalysis{} for _, pc := range podConfigs { var failures []common.Failure pod := pc.pod // Check upstream validation. // The Prometheus configuration structs do not generally have validation // methods and embed their validation logic in the UnmarshalYAML methods. config, err := unmarshalPromConfigBytes(pc.b) if err != nil { failures = append(failures, common.Failure{ Text: fmt.Sprintf("error validating Prometheus YAML configuration: %s", err), }) } _, err = yaml.Marshal(config) if err != nil { failures = append(failures, common.Failure{ Text: fmt.Sprintf("error validating Prometheus struct configuration: %s", err), }) } // Check for empty scrape config. if len(config.ScrapeConfigs) == 0 { failures = append(failures, common.Failure{ Text: "no scrape configurations. Prometheus will not scrape any metrics.", }) } if len(failures) > 0 { preAnalysis[fmt.Sprintf("%s/%s", pod.Namespace, pod.Name)] = common.PreAnalysis{ Pod: *pod, FailureDetails: failures, } } } for key, value := range preAnalysis { var currentAnalysis = common.Result{ Kind: kind, Name: key, Error: value.FailureDetails, } parent, _ := util.GetParent(a.Client, value.Pod.ObjectMeta) currentAnalysis.ParentObject = parent a.Results = append(a.Results, currentAnalysis) } return a.Results, nil } func configKey(namespace string, volume *corev1.Volume) (string, error) { if volume.ConfigMap != nil { return fmt.Sprintf("configmap/%s/%s", namespace, volume.ConfigMap.Name), nil } else if volume.Secret != nil { return fmt.Sprintf("secret/%s/%s", namespace, volume.Secret.SecretName), nil } else { return "", errors.New("volume format must be ConfigMap or Secret") } } func findPrometheusPodConfigs(ctx context.Context, client kubernetes.Interface, namespace string) ([]podConfig, error) { var configs []podConfig pods, err := findPrometheusPods(ctx, client, namespace) if err != nil { return nil, err } var configCache = make(map[string]bool) for _, pod := range pods { // Extract volume of Prometheus config. volume, key, err := findPrometheusConfigVolumeAndKey(ctx, client, &pod) if err != nil { return nil, err } // See if we processed it already; if so, don't process again. ck, err := configKey(pod.Namespace, volume) if err != nil { return nil, err } _, ok := configCache[ck] if ok { continue } configCache[ck] = true // Extract Prometheus config bytes from volume. b, err := extractPrometheusConfigFromVolume(ctx, client, volume, pod.Namespace, key) if err != nil { return nil, err } configs = append(configs, podConfig{ pod: &pod, b: b, }) } return configs, nil } func findPrometheusPods(ctx context.Context, client kubernetes.Interface, namespace string) ([]corev1.Pod, error) { var proms []corev1.Pod for k, v := range prometheusPodLabels { pods, err := util.GetPodListByLabels(client, namespace, map[string]string{ k: v, }) if err != nil { return nil, err } proms = append(proms, pods.Items...) } // If we still haven't found any Prometheus pods, make a last-ditch effort to // scrape the namespace for "prometheus" containers. if len(proms) == 0 { pods, err := client.CoreV1().Pods(namespace).List(ctx, v1.ListOptions{}) if err != nil { return nil, err } for _, pod := range pods.Items { for _, c := range pod.Spec.Containers { if c.Name == prometheusContainerName { proms = append(proms, pod) } } } } return proms, nil } func findPrometheusConfigPath(ctx context.Context, client kubernetes.Interface, pod *corev1.Pod) (string, error) { var path string var err error for _, container := range pod.Spec.Containers { for _, arg := range container.Args { // Prefer the config-reloader container config file as it normally // references the ConfigMap or Secret volume mount. // Fallback to the prometheus container if that's not found. if strings.HasPrefix(arg, prometheusConfigFlag) { path = strings.TrimPrefix(arg, prometheusConfigFlag) } if strings.HasPrefix(arg, configReloaderConfigFlag) { path = strings.TrimPrefix(arg, configReloaderConfigFlag) } } if container.Name == configReloaderContainerName { return path, nil } } if path == "" { err = fmt.Errorf("prometheus config path not found in pod: %s", pod.Name) } return path, err } func findPrometheusConfigVolumeAndKey(ctx context.Context, client kubernetes.Interface, pod *corev1.Pod) (*corev1.Volume, string, error) { path, err := findPrometheusConfigPath(ctx, client, pod) if err != nil { return nil, "", err } // Find the volumeMount the config path is pointing to. var volumeName = "" for _, container := range pod.Spec.Containers { for _, vm := range container.VolumeMounts { if strings.HasPrefix(path, vm.MountPath) { volumeName = vm.Name break } } } // Get the actual Volume from the name. for _, volume := range pod.Spec.Volumes { if volume.Name == volumeName { return &volume, filepath.Base(path), nil } } return nil, "", errors.New("volume for Prometheus config not found") } func extractPrometheusConfigFromVolume(ctx context.Context, client kubernetes.Interface, volume *corev1.Volume, namespace, key string) ([]byte, error) { var b []byte var ok bool // Check for Secret volume. if vs := volume.Secret; vs != nil { s, err := client.CoreV1().Secrets(namespace).Get(ctx, vs.SecretName, v1.GetOptions{}) if err != nil { return nil, err } b, ok = s.Data[key] if !ok { return nil, fmt.Errorf("unable to find file key in secret: %s", key) } } // Check for ConfigMap volume. if vcm := volume.ConfigMap; vcm != nil { cm, err := client.CoreV1().ConfigMaps(namespace).Get(ctx, vcm.Name, v1.GetOptions{}) if err != nil { return nil, err } s, ok := cm.Data[key] b = []byte(s) if !ok { return nil, fmt.Errorf("unable to find file key in configmap: %s", key) } } return b, nil } func unmarshalPromConfigBytes(b []byte) (*promconfig.Config, error) { var config promconfig.Config // Unmarshal the data into a Prometheus config. if err := yaml.Unmarshal(b, &config); err == nil { return &config, nil // If there were errors, try gunziping the data. } else if content := http.DetectContentType(b); content == "application/x-gzip" { r, err := gzip.NewReader(bytes.NewBuffer(b)) if err != nil { return &config, err } gunzipBytes, err := io.ReadAll(r) if err != nil { return &config, err } err = yaml.Unmarshal(gunzipBytes, &config) if err != nil { return nil, err } return &config, nil } else { return &config, err } } ================================================ FILE: pkg/integration/prometheus/prometheus.go ================================================ package prometheus import ( "context" "errors" "fmt" "os" "github.com/fatih/color" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" "github.com/spf13/viper" ) const ( ConfigValidate = "PrometheusConfigValidate" ConfigRelabel = "PrometheusConfigRelabelReport" ) type Prometheus struct { } func NewPrometheus() *Prometheus { return &Prometheus{} } func (p *Prometheus) Deploy(namespace string) error { // no-op color.Green("Activating prometheus integration...") // TODO(pintohutch): add timeout or inherit an upstream context // for better signal management. ctx := context.Background() kubecontext := viper.GetString("kubecontext") kubeconfig := viper.GetString("kubeconfig") client, err := kubernetes.NewClient(kubecontext, kubeconfig) if err != nil { color.Red("Error initialising kubernetes client: %v", err) os.Exit(1) } // We just care about existing deployments. // Try and find Prometheus configurations in the cluster using the provided namespace. // // Note: We could cache this state and inject it into the various analyzers // to save additional parsing later. // However, the state of the cluster can change from activation to analysis, // so we would want to run this again on each analyze call anyway. // // One consequence of this is one can run `activate` in one namespace // and run `analyze` in another, without issues, as long as Prometheus // is found in both. // We accept this as a trade-off for the time-being to avoid having the tool // manage Prometheus on the behalf of users. podConfigs, err := findPrometheusPodConfigs(ctx, client.GetClient(), namespace) if err != nil { color.Red("Error discovering Prometheus workloads: %v", err) os.Exit(1) } if len(podConfigs) == 0 { color.Yellow(fmt.Sprintf(`Prometheus installation not found in namespace: %s. Please ensure Prometheus is deployed to analyze.`, namespace)) return errors.New("no prometheus installation found") } // Prime state of the analyzer so color.Green("Found existing installation") return nil } func (p *Prometheus) UnDeploy(_ string) error { // no-op // We just care about existing deployments. color.Yellow("Integration will leave Prometheus resources deployed. This is an effective no-op in the cluster.") return nil } func (p *Prometheus) AddAnalyzer(mergedMap *map[string]common.IAnalyzer) { (*mergedMap)[ConfigValidate] = &ConfigAnalyzer{} (*mergedMap)[ConfigRelabel] = &RelabelAnalyzer{} } func (p *Prometheus) GetAnalyzerName() []string { return []string{ConfigValidate, ConfigRelabel} } func (p *Prometheus) GetNamespace() (string, error) { return "", nil } func (p *Prometheus) OwnsAnalyzer(analyzer string) bool { return (analyzer == ConfigValidate) || (analyzer == ConfigRelabel) } func (t *Prometheus) IsActivate() bool { activeFilters := viper.GetStringSlice("active_filters") for _, filter := range t.GetAnalyzerName() { for _, af := range activeFilters { if af == filter { return true } } } return false } ================================================ FILE: pkg/integration/prometheus/relabel_analyzer.go ================================================ package prometheus import ( "fmt" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/util" discoverykube "github.com/prometheus/prometheus/discovery/kubernetes" "gopkg.in/yaml.v2" ) type RelabelAnalyzer struct { } func (r *RelabelAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) { ctx := a.Context client := a.Client.GetClient() namespace := a.Namespace kind := ConfigRelabel podConfigs, err := findPrometheusPodConfigs(ctx, client, namespace) if err != nil { return nil, err } var preAnalysis = map[string]common.PreAnalysis{} for _, pc := range podConfigs { var failures []common.Failure pod := pc.pod // Check upstream validation. // The Prometheus configuration structs do not generally have validation // methods and embed their validation logic in the UnmarshalYAML methods. config, _ := unmarshalPromConfigBytes(pc.b) // Limit output for brevity. limit := 6 i := 0 for _, sc := range config.ScrapeConfigs { if i == limit { break } if sc == nil { continue } brc, _ := yaml.Marshal(sc.RelabelConfigs) var bsd []byte for _, cfg := range sc.ServiceDiscoveryConfigs { ks, ok := cfg.(*discoverykube.SDConfig) if !ok { continue } bsd, _ = yaml.Marshal(ks) } // Don't bother with relabel analysis if the scrape config // or service discovery config are empty. if len(brc) == 0 || len(bsd) == 0 { continue } failures = append(failures, common.Failure{ Text: fmt.Sprintf("job_name:\n%s\nrelabel_configs:\n%s\nkubernetes_sd_configs:\n%s\n", sc.JobName, string(brc), string(bsd)), }) i++ } if len(failures) > 0 { preAnalysis[fmt.Sprintf("%s/%s", pod.Namespace, pod.Name)] = common.PreAnalysis{ Pod: *pod, FailureDetails: failures, } } } for key, value := range preAnalysis { var currentAnalysis = common.Result{ Kind: kind, Name: key, Error: value.FailureDetails, } parent, _ := util.GetParent(a.Client, value.Pod.ObjectMeta) currentAnalysis.ParentObject = parent a.Results = append(a.Results, currentAnalysis) } return a.Results, nil } ================================================ FILE: pkg/kubernetes/apireference.go ================================================ package kubernetes import ( "fmt" "strings" openapi_v2 "github.com/google/gnostic/openapiv2" ) func (k *K8sApiReference) GetApiDocV2(field string) string { startPoint := "" // the path must be formated like "path1.path2.path3" paths := strings.Split(field, ".") group := strings.Split(k.ApiVersion.Group, ".") definitions := k.OpenapiSchema.GetDefinitions().GetAdditionalProperties() // extract the startpoint by searching the highest leaf corresponding to the requested group qnd kind for _, prop := range definitions { if strings.HasSuffix(prop.GetName(), fmt.Sprintf("%s.%s.%s", group[0], k.ApiVersion.Version, k.Kind)) { startPoint = prop.GetName() break } } // recursively parse the definitions to find the description of the latest part of the given path description := k.recursePath(definitions, startPoint, paths) return description } func (k *K8sApiReference) recursePath(definitions []*openapi_v2.NamedSchema, leaf string, paths []string) string { description := "" for _, prop := range definitions { // search the requested leaf if prop.GetName() == leaf { for _, addProp := range prop.GetValue().GetProperties().GetAdditionalProperties() { // search the additional property of the leaf corresponding the current path if addProp.GetName() == paths[0] { // the last path or the path is string, we get the description and we go out if len(paths) == 1 || addProp.GetValue().GetType().String() == "value:\"string\"" { // extract the path description as we are at the end of the paths description = addProp.GetValue().Description } else { // the path is an object, we extract the xref if addProp.GetValue().GetXRef() != "" { splitRef := strings.Split(addProp.GetValue().GetXRef(), "/") reducedPaths := paths[1:] description = k.recursePath(definitions, splitRef[len(splitRef)-1], reducedPaths) } // the path is an array, we take the first xref from the items if len(addProp.GetValue().GetItems().GetSchema()) == 1 { splitRef := strings.Split(addProp.GetValue().GetItems().GetSchema()[0].GetXRef(), "/") reducedPaths := paths[1:] description = k.recursePath(definitions, splitRef[len(splitRef)-1], reducedPaths) } } break } } break } } return description } ================================================ FILE: pkg/kubernetes/apireference_test.go ================================================ /* Copyright 2024 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package kubernetes import ( "testing" openapi_v2 "github.com/google/gnostic/openapiv2" "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/runtime/schema" ) func TestGetApiDocV2(t *testing.T) { k8s := &K8sApiReference{ ApiVersion: schema.GroupVersion{ Group: "group.v1", Version: "v1", }, OpenapiSchema: &openapi_v2.Document{ Definitions: &openapi_v2.Definitions{ AdditionalProperties: []*openapi_v2.NamedSchema{ { Name: "group.v1.kind", Value: &openapi_v2.Schema{ Title: "test", Properties: &openapi_v2.Properties{ AdditionalProperties: []*openapi_v2.NamedSchema{ { Name: "schema1", Value: &openapi_v2.Schema{ Title: "test", Description: "schema1 description", Type: &openapi_v2.TypeItem{ Value: []string{"string"}, }, }, }, { Name: "schema2", Value: &openapi_v2.Schema{ Items: &openapi_v2.ItemsItem{ Schema: []*openapi_v2.Schema{ { Title: "random-schema", }, }, }, Title: "test", XRef: "xref", Description: "schema2 description", Type: &openapi_v2.TypeItem{ Value: []string{"bool"}, }, }, }, }, }, }, }, { Name: "group", }, }, }, }, Kind: "kind", } tests := []struct { name string field string expectedOutput string }{ { name: "empty field", }, { name: "2 schemas", field: "schema2.schema1", expectedOutput: "", }, { name: "schema1 description", field: "schema1", expectedOutput: "schema1 description", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { output := k8s.GetApiDocV2(tt.field) require.Equal(t, tt.expectedOutput, output) }) } } ================================================ FILE: pkg/kubernetes/kubernetes.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package kubernetes import ( "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" _ "k8s.io/client-go/plugin/pkg/client/auth/oidc" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" ctrl "sigs.k8s.io/controller-runtime/pkg/client" ) func (c *Client) GetConfig() *rest.Config { return c.Config } func (c *Client) GetClient() kubernetes.Interface { return c.Client } func (c *Client) GetCtrlClient() ctrl.Client { return c.CtrlClient } func (c *Client) GetDynamicClient() dynamic.Interface { return c.DynamicClient } func NewClient(kubecontext string, kubeconfig string) (*Client, error) { var config *rest.Config config, err := rest.InClusterConfig() if kubeconfig != "" || err != nil { loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() if kubeconfig != "" { loadingRules.ExplicitPath = kubeconfig } clientConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig( loadingRules, &clientcmd.ConfigOverrides{ CurrentContext: kubecontext, }) // create the clientset config, err = clientConfig.ClientConfig() if err != nil { return nil, err } } clientSet, err := kubernetes.NewForConfig(config) if err != nil { return nil, err } ctrlClient, err := ctrl.New(config, ctrl.Options{}) if err != nil { return nil, err } serverVersion, err := clientSet.ServerVersion() if err != nil { return nil, err } dynamicClient, err := dynamic.NewForConfig(config) if err != nil { return nil, err } return &Client{ Client: clientSet, CtrlClient: ctrlClient, Config: config, ServerVersion: serverVersion, DynamicClient: dynamicClient, }, nil } ================================================ FILE: pkg/kubernetes/types.go ================================================ package kubernetes import ( openapi_v2 "github.com/google/gnostic/openapiv2" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/version" "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" ctrl "sigs.k8s.io/controller-runtime/pkg/client" ) type Client struct { Client kubernetes.Interface CtrlClient ctrl.Client Config *rest.Config ServerVersion *version.Info DynamicClient dynamic.Interface } type K8sApiReference struct { ApiVersion schema.GroupVersion Kind string OpenapiSchema *openapi_v2.Document } ================================================ FILE: pkg/server/README.md ================================================ # K8sGPT MCP Server 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. ## Components - `mcp.go`: The main MCP server implementation - `server.go`: The HTTP server implementation - `tools.go`: Tool definitions for the MCP server ## Features The MCP server provides the following features: 1. **Analyze Kubernetes Resources**: Analyze Kubernetes resources in a cluster 2. **Get Cluster Information**: Retrieve information about the Kubernetes cluster ## Usage To use the MCP server, you need to: 1. Initialize the MCP server with a Kubernetes client 2. Start the server 3. Connect to the server using an MCP client Example: ```go client, err := kubernetes.NewForConfig(config) if err != nil { log.Fatalf("Failed to create Kubernetes client: %v", err) } mcpServer := server.NewMCPServer(client) if err := mcpServer.Start(); err != nil { log.Fatalf("Failed to start MCP server: %v", err) } ``` ## Integration The MCP server can be integrated with other tools that support the MCP protocol, such as: - Mission Control - Other MCP-compatible tools ## License This code is licensed under the Apache License 2.0. ================================================ FILE: pkg/server/analyze/analyze.go ================================================ package analyze import ( "context" json "encoding/json" schemav1 "buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go/schema/v1" "github.com/k8sgpt-ai/k8sgpt/pkg/analysis" ) func (h *Handler) Analyze(ctx context.Context, i *schemav1.AnalyzeRequest) ( *schemav1.AnalyzeResponse, error, ) { if i.Output == "" { i.Output = "json" } if int(i.MaxConcurrency) == 0 { i.MaxConcurrency = 10 } config, err := analysis.NewAnalysis( i.Backend, i.Language, i.Filters, i.Namespace, i.LabelSelector, i.Nocache, i.Explain, int(i.MaxConcurrency), false, // Kubernetes Doc disabled in server mode false, // Interactive mode disabled in server mode []string{}, //TODO: add custom http headers in server mode false, // with stats disable ) if err != nil { return &schemav1.AnalyzeResponse{}, err } config.Context = ctx // Replace context for correct timeouts. defer config.Close() if config.CustomAnalyzersAreAvailable() { config.RunCustomAnalysis() } config.RunAnalysis() if i.Explain { err := config.GetAIResults(i.Output, i.Anonymize) if err != nil { return &schemav1.AnalyzeResponse{}, err } } out, err := config.PrintOutput(i.Output) if err != nil { return &schemav1.AnalyzeResponse{}, err } var obj schemav1.AnalyzeResponse err = json.Unmarshal(out, &obj) if err != nil { return &schemav1.AnalyzeResponse{}, err } return &obj, nil } ================================================ FILE: pkg/server/analyze/handler.go ================================================ package analyze import rpc "buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go/schema/v1/schemav1grpc" type Handler struct { rpc.UnimplementedServerAnalyzerServiceServer } ================================================ FILE: pkg/server/client_example/README.md ================================================ # K8sGPT MCP Client Example This directory contains an example of how to use the K8sGPT MCP client in a real-world scenario. ## Prerequisites - Go 1.16 or later - Access to a Kubernetes cluster - `kubectl` configured to access your cluster ## Building the Example To build the example, run: ```bash go build -o mcp-client-example ``` ## Running the Example To run the example, use the following command: ```bash ./mcp-client-example --kubeconfig=/path/to/kubeconfig --namespace=default ``` ### Command-line Flags - `--kubeconfig`: Path to the kubeconfig file (optional, defaults to the standard location) - `--namespace`: Kubernetes namespace to analyze (optional) ## Example Output When you run the example, you should see output similar to the following: ``` Starting MCP client... ``` The client will continue running until you press Ctrl+C to stop it. ## Integration with Mission Control To integrate this example with Mission Control, you need to: 1. Start the MCP client using the example 2. Configure Mission Control to connect to the MCP client 3. Use Mission Control to analyze your Kubernetes cluster ## Troubleshooting If you encounter any issues, check the following: 1. Ensure that your Kubernetes cluster is accessible 2. Verify that your kubeconfig file is valid 3. Check that the namespace you specified exists ## License This code is licensed under the Apache License 2.0. ================================================ FILE: pkg/server/client_example/main.go ================================================ /* Copyright 2024 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "bytes" "encoding/json" "flag" "fmt" "io" "log" "net/http" "time" ) // AnalyzeRequest represents the input parameters for the analyze tool type AnalyzeRequest struct { Namespace string `json:"namespace,omitempty"` Backend string `json:"backend,omitempty"` Language string `json:"language,omitempty"` Filters []string `json:"filters,omitempty"` LabelSelector string `json:"labelSelector,omitempty"` NoCache bool `json:"noCache,omitempty"` Explain bool `json:"explain,omitempty"` MaxConcurrency int `json:"maxConcurrency,omitempty"` WithDoc bool `json:"withDoc,omitempty"` InteractiveMode bool `json:"interactiveMode,omitempty"` CustomHeaders []string `json:"customHeaders,omitempty"` WithStats bool `json:"withStats,omitempty"` } // JSONRPCResponse represents the JSON-RPC response format type JSONRPCResponse struct { JSONRPC string `json:"jsonrpc"` ID int `json:"id"` Result struct { Content []struct { Text string `json:"text"` Type string `json:"type"` } `json:"content"` } `json:"result,omitempty"` Error *struct { Code int `json:"code"` Message string `json:"message"` } `json:"error,omitempty"` } func main() { // Parse command line flags serverPort := flag.String("port", "8089", "Port of the MCP server") namespace := flag.String("namespace", "", "Kubernetes namespace to analyze") backend := flag.String("backend", "", "AI backend to use") language := flag.String("language", "english", "Language for analysis") flag.Parse() // Create analyze request req := AnalyzeRequest{ Namespace: *namespace, Backend: *backend, Language: *language, Explain: true, MaxConcurrency: 10, } // Note: req is now used directly in the JSON-RPC request // Create HTTP client with timeout client := &http.Client{ Timeout: 5 * time.Minute, } // First, initialize the session initRequest := map[string]interface{}{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": map[string]interface{}{ "protocolVersion": "2025-03-26", "capabilities": map[string]interface{}{ "tools": map[string]interface{}{}, "resources": map[string]interface{}{}, "prompts": map[string]interface{}{}, }, "clientInfo": map[string]interface{}{ "name": "k8sgpt-client", "version": "1.0.0", }, }, } initData, err := json.Marshal(initRequest) if err != nil { log.Fatalf("Failed to marshal init request: %v", err) } // Send initialization request initResp, err := client.Post( fmt.Sprintf("http://localhost:%s/mcp", *serverPort), "application/json", bytes.NewBuffer(initData), ) if err != nil { log.Fatalf("Failed to send init request: %v", err) } defer func() { if err := initResp.Body.Close(); err != nil { log.Printf("Error closing init response body: %v", err) } }() // Extract session ID from response headers sessionID := initResp.Header.Get("Mcp-Session-Id") if sessionID == "" { log.Println("Warning: No session ID received from server") } // Create JSON-RPC request for analyze jsonRPCRequest := map[string]interface{}{ "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": map[string]interface{}{ "name": "analyze", "arguments": req, }, } // Convert to JSON jsonRPCData, err := json.Marshal(jsonRPCRequest) if err != nil { log.Fatalf("Failed to marshal JSON-RPC request: %v", err) } // Create request with session ID if available httpReq, err := http.NewRequest("POST", fmt.Sprintf("http://localhost:%s/mcp", *serverPort), bytes.NewBuffer(jsonRPCData)) if err != nil { log.Fatalf("Failed to create request: %v", err) } httpReq.Header.Set("Content-Type", "application/json") httpReq.Header.Set("Accept", "application/json,text/event-stream") if sessionID != "" { httpReq.Header.Set("Mcp-Session-Id", sessionID) } // Send request to MCP server resp, err := client.Do(httpReq) if err != nil { log.Fatalf("Failed to send request: %v", err) } defer func() { if err := resp.Body.Close(); err != nil { log.Printf("Error closing response body: %v", err) } }() // Read and print raw response for debugging body, err := io.ReadAll(resp.Body) if err != nil { log.Fatalf("Failed to read response body: %v", err) } fmt.Printf("Raw response: %s\n", string(body)) // Parse response var jsonRPCResp JSONRPCResponse if err := json.Unmarshal(body, &jsonRPCResp); err != nil { log.Fatalf("Failed to decode response: %v", err) } // Print results fmt.Println("Analysis Results:") if jsonRPCResp.Error != nil { fmt.Printf("Error: %s (code: %d)\n", jsonRPCResp.Error.Message, jsonRPCResp.Error.Code) } else if len(jsonRPCResp.Result.Content) > 0 { fmt.Println(jsonRPCResp.Result.Content[0].Text) } else { fmt.Println("No results returned") } } ================================================ FILE: pkg/server/config/config.go ================================================ package config import ( "context" schemav1 "buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go/schema/v1" "github.com/k8sgpt-ai/k8sgpt/pkg/cache" "github.com/k8sgpt-ai/k8sgpt/pkg/custom" "github.com/spf13/viper" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) const ( notUsedBucket = "" notUsedRegion = "" notUsedEndpoint = "" notUsedStorageAcc = "" notUsedContainerName = "" notUsedProjectId = "" notUsedInsecure = false ) // ApplyConfig applies the configuration changes from the request func (h *Handler) ApplyConfig(ctx context.Context, i *schemav1.AddConfigRequest) error { if i.CustomAnalyzers != nil { // We need to add the custom analyzers to the viper config and save them var customAnalyzers = make([]custom.CustomAnalyzer, 0) if err := viper.UnmarshalKey("custom_analyzers", &customAnalyzers); err != nil { return err } else { // If there are analyzers are already in the config we will append the ones with new names for _, ca := range i.CustomAnalyzers { exists := false for _, c := range customAnalyzers { if c.Name == ca.Name { exists = true break } } if !exists { customAnalyzers = append(customAnalyzers, custom.CustomAnalyzer{ Name: ca.Name, Connection: custom.Connection{ Url: ca.Connection.Url, Port: ca.Connection.Port, }, }) } } // save the config viper.Set("custom_analyzers", customAnalyzers) if err := viper.WriteConfig(); err != nil { return err } } } if i.Cache != nil { var err error var remoteCache cache.CacheProvider switch i.Cache.GetCacheType().(type) { case *schemav1.Cache_AzureCache: remoteCache, err = cache.NewCacheProvider("azure", notUsedBucket, notUsedRegion, notUsedEndpoint, i.Cache.GetAzureCache().StorageAccount, i.Cache.GetAzureCache().ContainerName, notUsedProjectId, notUsedInsecure) case *schemav1.Cache_S3Cache: remoteCache, err = cache.NewCacheProvider("s3", i.Cache.GetS3Cache().BucketName, i.Cache.GetS3Cache().Region, i.Cache.GetS3Cache().Endpoint, notUsedStorageAcc, notUsedContainerName, notUsedProjectId, i.Cache.GetS3Cache().Insecure) case *schemav1.Cache_GcsCache: remoteCache, err = cache.NewCacheProvider("gcs", i.Cache.GetGcsCache().BucketName, i.Cache.GetGcsCache().Region, notUsedEndpoint, notUsedStorageAcc, notUsedContainerName, i.Cache.GetGcsCache().GetProjectId(), notUsedInsecure) case *schemav1.Cache_InterplexCache: remoteCache, err = cache.NewCacheProvider("interplex", notUsedBucket, notUsedRegion, i.Cache.GetInterplexCache().Endpoint, notUsedStorageAcc, notUsedContainerName, notUsedProjectId, notUsedInsecure) default: return status.Error(codes.InvalidArgument, "Invalid cache configuration") } if err != nil { return err } err = cache.AddRemoteCache(remoteCache) if err != nil { return err } } return nil } func (h *Handler) AddConfig(ctx context.Context, i *schemav1.AddConfigRequest) (*schemav1.AddConfigResponse, error) { resp, err := h.syncIntegration(ctx, i) if err != nil { return resp, err } if err := h.ApplyConfig(ctx, i); err != nil { return resp, err } return resp, nil } func (h *Handler) RemoveConfig(ctx context.Context, i *schemav1.RemoveConfigRequest) (*schemav1.RemoveConfigResponse, error, ) { err := cache.RemoveRemoteCache() if err != nil { return &schemav1.RemoveConfigResponse{}, err } // Remove any integrations is a TBD as it would be nice to make this more granular // Currently integrations can be removed in the AddConfig sync return &schemav1.RemoveConfigResponse{ Status: "Successfully removed the remote cache", }, nil } ================================================ FILE: pkg/server/config/handler.go ================================================ package config import ( rpc "buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go/schema/v1/schemav1grpc" schemav1 "buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go/schema/v1" "context" ) type Handler struct { rpc.UnimplementedServerConfigServiceServer } func (h *Handler) Shutdown(ctx context.Context, request *schemav1.ShutdownRequest) (*schemav1.ShutdownResponse, error) { //TODO implement me panic("implement me") } ================================================ FILE: pkg/server/config/integration.go ================================================ package config import ( "context" "fmt" schemav1 "buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go/schema/v1" "github.com/k8sgpt-ai/k8sgpt/pkg/integration" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) // syncIntegration is aware of the following events // A new integration added // An integration removed from the Integration block func (h *Handler) syncIntegration(ctx context.Context, i *schemav1.AddConfigRequest) (*schemav1.AddConfigResponse, error, ) { fmt.Println("WARNING: syncIntegration is deprecated.") response := &schemav1.AddConfigResponse{} integrationProvider := integration.NewIntegration() if i.Integrations == nil { // If there are locally activate integrations, disable them err := h.deactivateAllIntegrations(integrationProvider) if err != nil { return response, status.Error(codes.NotFound, "deactivation error") } return response, nil } // Warning: This code is an example of an integration modifying the active filter list // This integration is no longer part of K8sGPT due to compatibility issues //coreFilters, _, _ := analyzer.ListFilters() // Update filters //activeFilters := viper.GetStringSlice("active_filters") //if len(activeFilters) == 0 { // activeFilters = coreFilters //} //var err error = status.Error(codes.OK, "") //if err != nil { // fmt.Println(err) //} //deactivateFunc := func(integrationRef integration.IIntegration) error { // namespace, err := integrationRef.GetNamespace() // if err != nil { // return err // } // err = integrationProvider.Deactivate(trivyName, namespace) // if err != nil { // return status.Error(codes.NotFound, "integration already deactivated") // } // return nil //} //integrationRef, err := integrationProvider.Get(trivyName) //if err != nil { // return response, status.Error(codes.NotFound, "provider get failure") //} //if i.Integrations.Trivy != nil { // switch i.Integrations.Trivy.Enabled { // case true: // if b, err := integrationProvider.IsActivate(trivyName); err != nil { // return response, status.Error(codes.Internal, "integration activation error") // } else { // if !b { // err := integrationProvider.Activate(trivyName, i.Integrations.Trivy.Namespace, // activeFilters, i.Integrations.Trivy.SkipInstall) // if err != nil { // return nil, err // } // } else { // return response, status.Error(codes.AlreadyExists, "integration already active") // } // } // case false: // err = deactivateFunc(integrationRef) // if err != nil { // return nil, err // } // // This break is included purely for static analysis to pass // } //} else { // // If Trivy has been removed, disable it // err = deactivateFunc(integrationRef) // if err != nil { // return nil, err // } //} return response, nil } func (h *Handler) ListIntegrations(ctx context.Context, req *schemav1.ListIntegrationsRequest) (*schemav1.ListIntegrationsResponse, error) { fmt.Println("WARNING: ListIntegrations is deprecated.") //integrationProvider := integration.NewIntegration() // Update the requester with the status of Trivy //trivy, err := integrationProvider.Get(trivyName) //active := trivy.IsActivate() //var skipInstall bool //var namespace string = "" //if active { // namespace, err = trivy.GetNamespace() // if err != nil { // return nil, status.Error(codes.NotFound, "namespace not found") // } // if namespace == "" { // skipInstall = true // } //} // //if err != nil { // return nil, status.Error(codes.NotFound, "trivy integration") //} resp := &schemav1.ListIntegrationsResponse{ //Trivy: &schemav1.Trivy{ // Enabled: active, // Namespace: namespace, // SkipInstall: skipInstall, //}, } return resp, nil } func (*Handler) deactivateAllIntegrations(integrationProvider *integration.Integration) error { fmt.Println("WARNING: deactivateIntegrations is deprecated.") integrations := integrationProvider.List() for _, i := range integrations { b, _ := integrationProvider.IsActivate(i) if b { in, err := integrationProvider.Get(i) if err != nil { return err } namespace, err := in.GetNamespace() if err != nil { return err } if err == nil { if namespace != "" { err := integrationProvider.Deactivate(i, namespace) if err != nil { return err } } else { fmt.Printf("Skipping deactivation of %s, not installed\n", i) } } else { return err } } } return nil } ================================================ FILE: pkg/server/example/main.go ================================================ /* Copyright 2024 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "flag" "log" "os" "os/signal" "syscall" "github.com/k8sgpt-ai/k8sgpt/pkg/ai" "github.com/k8sgpt-ai/k8sgpt/pkg/server" "go.uber.org/zap" ) func main() { // Parse command line flags port := flag.String("port", "8089", "Port to run the MCP server on") useHTTP := flag.Bool("http", false, "Enable HTTP mode for MCP server") flag.Parse() // Initialize zap logger logger, err := zap.NewProduction() if err != nil { log.Fatalf("Error creating logger: %v", err) } defer func() { if err := logger.Sync(); err != nil { log.Printf("Error syncing logger: %v", err) } }() // Create AI provider aiProvider := &ai.AIProvider{ Name: "openai", Password: os.Getenv("OPENAI_API_KEY"), Model: "gpt-3.5-turbo", } // Create and start MCP server mcpServer, err := server.NewMCPServer(*port, aiProvider, *useHTTP, logger) if err != nil { log.Fatalf("Error creating MCP server: %v", err) } // Start the server in a goroutine go func() { if err := mcpServer.Start(); err != nil { log.Fatalf("Error starting MCP server: %v", err) } }() // Handle graceful shutdown sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) <-sigChan // Cleanup if err := mcpServer.Close(); err != nil { log.Printf("Error closing MCP server: %v", err) } } ================================================ FILE: pkg/server/log.go ================================================ package server import ( "context" "fmt" "time" "go.uber.org/zap" "google.golang.org/grpc" "google.golang.org/grpc/peer" "google.golang.org/grpc/status" ) func LogInterceptor(logger *zap.Logger) grpc.UnaryServerInterceptor { return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { start := time.Now() // Call the handler to execute the gRPC request response, err := handler(ctx, req) duration := time.Since(start).Milliseconds() fields := []zap.Field{ zap.Int64("duration_ms", duration), zap.String("method", info.FullMethod), zap.Any("request", req), } // Get the remote address from the context peer, ok := peer.FromContext(ctx) if ok { fields = append(fields, zap.String("remote_addr", peer.Addr.String())) } if err != nil { fields = append(fields, zap.Int32("status_code", int32(status.Code(err)))) } message := "request completed" if err != nil { message = fmt.Sprintf("request failed. %s", err.Error()) } logRequest(logger, fields, int(status.Code(err)), message) return response, err } } func logRequest(logger *zap.Logger, fields []zap.Field, statusCode int, message string) { if statusCode >= 400 { logger.Error(message, fields...) } else { logger.Info(message, fields...) } } ================================================ FILE: pkg/server/mcp.go ================================================ /* Copyright 2024 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "context" "encoding/json" "fmt" "regexp" schemav1 "buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go/schema/v1" "github.com/k8sgpt-ai/k8sgpt/pkg/ai" "github.com/k8sgpt-ai/k8sgpt/pkg/analysis" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" "github.com/k8sgpt-ai/k8sgpt/pkg/server/config" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" "github.com/spf13/viper" "go.uber.org/zap" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // K8sGptMCPServer represents an MCP server for k8sgpt type K8sGptMCPServer struct { server *server.MCPServer port string aiProvider *ai.AIProvider useHTTP bool logger *zap.Logger httpServer *server.StreamableHTTPServer stdioServer *server.StdioServer } func NewMCPServer(port string, aiProvider *ai.AIProvider, useHTTP bool, logger *zap.Logger) (*K8sGptMCPServer, error) { opts := []server.ServerOption{ server.WithToolCapabilities(true), server.WithResourceCapabilities(true, false), server.WithPromptCapabilities(false), } // Create the MCP server mcpServer := server.NewMCPServer("k8sgpt", "1.0.0", opts...) var k8sGptMCPServer = &K8sGptMCPServer{ server: mcpServer, port: port, aiProvider: aiProvider, useHTTP: useHTTP, logger: logger, } // Register tools and resources immediately if err := k8sGptMCPServer.registerToolsAndResources(); err != nil { return nil, fmt.Errorf("failed to register tools and resources: %v", err) } if useHTTP { // Create HTTP server with streamable transport httpOpts := []server.StreamableHTTPOption{ server.WithLogger(&zapLoggerAdapter{logger: logger}), // Enable stateless mode for one-off tool invocations without session management server.WithStateLess(true), } httpServer := server.NewStreamableHTTPServer(mcpServer, httpOpts...) // Launch the HTTP server directly go func() { logger.Info("Starting MCP HTTP server", zap.String("port", port)) if err := httpServer.Start(":" + port); err != nil { logger.Fatal("MCP HTTP server failed", zap.Error(err)) } }() return &K8sGptMCPServer{ server: mcpServer, port: port, aiProvider: aiProvider, useHTTP: useHTTP, logger: logger, httpServer: httpServer, }, nil } else { // Create stdio server stdioServer := server.NewStdioServer(mcpServer) return &K8sGptMCPServer{ server: mcpServer, port: port, aiProvider: aiProvider, useHTTP: useHTTP, logger: logger, stdioServer: stdioServer, }, nil } } // Start starts the MCP server func (s *K8sGptMCPServer) Start() error { if s.server == nil { return fmt.Errorf("server not initialized") } // Register prompts if err := s.registerPrompts(); err != nil { return fmt.Errorf("failed to register prompts: %v", err) } // Register resources if err := s.registerResources(); err != nil { return fmt.Errorf("failed to register resources: %v", err) } // Start the server based on transport type if s.useHTTP { // HTTP server is already running in a goroutine return nil } else { // Start stdio server (this will block) return server.ServeStdio(s.server) } } func (s *K8sGptMCPServer) registerToolsAndResources() error { // Register analyze tool with proper JSON schema analyzeTool := mcp.NewTool("analyze", mcp.WithDescription("Analyze Kubernetes resources for issues and problems"), mcp.WithString("namespace", mcp.Description("Kubernetes namespace to analyze (empty for all namespaces)"), ), mcp.WithString("backend", mcp.Description("AI backend to use for analysis (e.g., openai, azure, localai)"), ), mcp.WithBoolean("explain", mcp.Description("Provide detailed explanations for issues"), ), mcp.WithArray("filters", mcp.Description("Provide filters to narrow down the analysis (e.g. ['Pods', 'Deployments'])"), // without below line MCP server fails with Google Agent Development Kit (ADK), interestingly works fine with mcpinspector mcp.WithStringItems(), ), ) s.server.AddTool(analyzeTool, s.handleAnalyze) // Register cluster info tool (no parameters needed) clusterInfoTool := mcp.NewTool("cluster-info", mcp.WithDescription("Get Kubernetes cluster information and version"), ) s.server.AddTool(clusterInfoTool, s.handleClusterInfo) // Register config tool with proper JSON schema configTool := mcp.NewTool("config", mcp.WithDescription("Configure K8sGPT settings including custom analyzers and cache"), mcp.WithObject("customAnalyzers", mcp.Description("Custom analyzer configurations"), mcp.Properties(map[string]any{ "name": map[string]any{ "type": "string", "description": "Name of the custom analyzer", }, "connection": map[string]any{ "type": "object", "properties": map[string]any{ "url": map[string]any{ "type": "string", "description": "URL of the custom analyzer service", }, "port": map[string]any{ "type": "integer", "description": "Port of the custom analyzer service", }, }, }, }), ), mcp.WithObject("cache", mcp.Description("Cache configuration"), mcp.Properties(map[string]any{ "type": map[string]any{ "type": "string", "description": "Cache type (s3, azure, gcs)", "enum": []string{"s3", "azure", "gcs"}, }, "bucketName": map[string]any{ "type": "string", "description": "Bucket name for S3/GCS cache", }, "region": map[string]any{ "type": "string", "description": "Region for S3/GCS cache", }, "endpoint": map[string]any{ "type": "string", "description": "Custom endpoint for S3 cache", }, "insecure": map[string]any{ "type": "boolean", "description": "Use insecure connection for cache", }, "storageAccount": map[string]any{ "type": "string", "description": "Storage account for Azure cache", }, "containerName": map[string]any{ "type": "string", "description": "Container name for Azure cache", }, "projectId": map[string]any{ "type": "string", "description": "Project ID for GCS cache", }, }), ), ) s.server.AddTool(configTool, s.handleConfig) // Register resource listing tools listResourcesTool := mcp.NewTool("list-resources", mcp.WithDescription("List Kubernetes resources of a specific type (pods, deployments, services, nodes, etc.)"), mcp.WithString("resourceType", mcp.Required(), mcp.Description("Type of resource to list (e.g., pods, deployments, services, nodes, jobs, etc.)"), ), mcp.WithString("namespace", mcp.Description("Namespace to list resources from (empty for all or cluster-scoped resources)"), ), mcp.WithString("labelSelector", mcp.Description("Label selector to filter resources (e.g., 'app=myapp')"), ), ) s.server.AddTool(listResourcesTool, s.handleListResources) // Register get resource tool getResourceTool := mcp.NewTool("get-resource", mcp.WithDescription("Get detailed information about a specific Kubernetes resource"), mcp.WithString("resourceType", mcp.Required(), mcp.Description("Type of resource (e.g., pod, deployment, service)"), ), mcp.WithString("name", mcp.Required(), mcp.Description("Name of the resource"), ), mcp.WithString("namespace", mcp.Description("Namespace of the resource (required for namespaced resources)"), ), ) s.server.AddTool(getResourceTool, s.handleGetResource) // Register list namespaces tool listNamespacesTool := mcp.NewTool("list-namespaces", mcp.WithDescription("List all namespaces in the cluster"), ) s.server.AddTool(listNamespacesTool, s.handleListNamespaces) // Register list events tool listEventsTool := mcp.NewTool("list-events", mcp.WithDescription("List Kubernetes events for debugging and troubleshooting"), mcp.WithString("namespace", mcp.Description("Namespace to list events from (empty for all namespaces)"), ), mcp.WithString("involvedObjectName", mcp.Description("Filter events by involved object name (e.g., pod name)"), ), mcp.WithString("involvedObjectKind", mcp.Description("Filter events by involved object kind (e.g., Pod, Deployment)"), ), mcp.WithNumber("limit", mcp.Description("Maximum number of events to return (default: 100)"), ), ) s.server.AddTool(listEventsTool, s.handleListEvents) // Register get logs tool getLogsTool := mcp.NewTool("get-logs", mcp.WithDescription("Get logs from a pod container"), mcp.WithString("podName", mcp.Required(), mcp.Description("Name of the pod"), ), mcp.WithString("namespace", mcp.Required(), mcp.Description("Namespace of the pod"), ), mcp.WithString("container", mcp.Description("Container name (if pod has multiple containers)"), ), mcp.WithBoolean("previous", mcp.Description("Get logs from previous terminated container"), ), mcp.WithNumber("tailLines", mcp.Description("Number of lines from the end of logs (default: 100)"), ), mcp.WithNumber("sinceSeconds", mcp.Description("Return logs newer than this many seconds"), ), ) s.server.AddTool(getLogsTool, s.handleGetLogs) // Register filter management tools listFiltersTool := mcp.NewTool("list-filters", mcp.WithDescription("List all available and active analyzers/filters in k8sgpt"), ) s.server.AddTool(listFiltersTool, s.handleListFilters) addFiltersTool := mcp.NewTool("add-filters", mcp.WithDescription("Add filters to enable specific analyzers"), mcp.WithArray("filters", mcp.Required(), mcp.Description("List of filter names to add (e.g., ['Pod', 'Service', 'Deployment'])"), mcp.WithStringItems(), ), ) s.server.AddTool(addFiltersTool, s.handleAddFilters) removeFiltersTool := mcp.NewTool("remove-filters", mcp.WithDescription("Remove filters to disable specific analyzers"), mcp.WithArray("filters", mcp.Required(), mcp.Description("List of filter names to remove"), mcp.WithStringItems(), ), ) s.server.AddTool(removeFiltersTool, s.handleRemoveFilters) // Register integration management tools listIntegrationsTool := mcp.NewTool("list-integrations", mcp.WithDescription("List available integrations (Prometheus, AWS, Keda, Kyverno, etc.)"), ) s.server.AddTool(listIntegrationsTool, s.handleListIntegrations) return nil } // AnalyzeRequest represents the input parameters for the analyze tool type AnalyzeRequest struct { Namespace string `json:"namespace,omitempty"` Backend string `json:"backend,omitempty"` Language string `json:"language,omitempty"` Filters []string `json:"filters,omitempty"` LabelSelector string `json:"labelSelector,omitempty"` NoCache bool `json:"noCache,omitempty"` Explain bool `json:"explain,omitempty"` MaxConcurrency int `json:"maxConcurrency,omitempty"` WithDoc bool `json:"withDoc,omitempty"` InteractiveMode bool `json:"interactiveMode,omitempty"` CustomHeaders []string `json:"customHeaders,omitempty"` WithStats bool `json:"withStats,omitempty"` Anonymize bool `json:"anonymize,omitempty"` } // AnalyzeResponse represents the output of the analyze tool type AnalyzeResponse struct { Results string `json:"results"` } // ClusterInfoRequest represents the input parameters for the cluster-info tool type ClusterInfoRequest struct { // Empty struct as we don't need any input parameters } // ClusterInfoResponse represents the output of the cluster-info tool type ClusterInfoResponse struct { Info string `json:"info"` } // ConfigRequest represents the input parameters for the config tool type ConfigRequest struct { CustomAnalyzers []struct { Name string `json:"name"` Connection struct { Url string `json:"url"` Port int `json:"port"` } `json:"connection"` } `json:"customAnalyzers,omitempty"` Cache struct { Type string `json:"type"` // S3 specific fields BucketName string `json:"bucketName,omitempty"` Region string `json:"region,omitempty"` Endpoint string `json:"endpoint,omitempty"` Insecure bool `json:"insecure,omitempty"` // Azure specific fields StorageAccount string `json:"storageAccount,omitempty"` ContainerName string `json:"containerName,omitempty"` // GCS specific fields ProjectId string `json:"projectId,omitempty"` } `json:"cache,omitempty"` } // ConfigResponse represents the output of the config tool type ConfigResponse struct { Status string `json:"status"` } // handleAnalyze handles the analyze tool func (s *K8sGptMCPServer) handleAnalyze(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { var req AnalyzeRequest if err := request.BindArguments(&req); err != nil { return mcp.NewToolResultErrorf("Failed to parse request arguments: %v", err), nil } if req.Backend == "" { if s.aiProvider.Name != "" { req.Backend = s.aiProvider.Name } else { req.Backend = "openai" // fallback default } } // Get stored filters if not specified if len(req.Filters) == 0 { req.Filters = viper.GetStringSlice("active_filters") } // Validate MaxConcurrency to prevent excessive memory allocation req.MaxConcurrency = validateMaxConcurrency(req.MaxConcurrency) // Create a new analysis with the request parameters analysis, err := analysis.NewAnalysis( req.Backend, req.Language, req.Filters, req.Namespace, req.LabelSelector, req.NoCache, req.Explain, req.MaxConcurrency, req.WithDoc, req.InteractiveMode, req.CustomHeaders, req.WithStats, ) if err != nil { return mcp.NewToolResultErrorf("Failed to create analysis: %v", err), nil } defer analysis.Close() // Run the analysis analysis.RunAnalysis() if req.Explain { var output string err := analysis.GetAIResults(output, req.Anonymize) if err != nil { return mcp.NewToolResultErrorf("Failed to get results from AI: %v", err), nil } // Convert results to JSON string using PrintOutput outputBytes, err := analysis.PrintOutput("text") if err != nil { return mcp.NewToolResultErrorf("Failed to convert results to string: %v", err), nil } plainText := stripANSI(string(outputBytes)) return mcp.NewToolResultText(plainText), nil } else { // Get the output output, err := analysis.PrintOutput("json") if err != nil { return mcp.NewToolResultErrorf("Failed to print output: %v", err), nil } return mcp.NewToolResultText(string(output)), nil } } // validateMaxConcurrency validates and bounds the MaxConcurrency parameter func validateMaxConcurrency(maxConcurrency int) int { const maxAllowedConcurrency = 100 if maxConcurrency <= 0 { return 10 // Default value if not set } else if maxConcurrency > maxAllowedConcurrency { return maxAllowedConcurrency // Cap at a reasonable maximum } return maxConcurrency } // handleClusterInfo handles the cluster-info tool func (s *K8sGptMCPServer) handleClusterInfo(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { // Create a new Kubernetes client client, err := kubernetes.NewClient("", "") if err != nil { return mcp.NewToolResultErrorf("failed to create Kubernetes client: %v", err), nil } // Get cluster info from the client version, err := client.Client.Discovery().ServerVersion() if err != nil { return mcp.NewToolResultErrorf("failed to get cluster version: %v", err), nil } info := fmt.Sprintf("Kubernetes %s", version.GitVersion) return mcp.NewToolResultText(info), nil } // handleConfig handles the config tool func (s *K8sGptMCPServer) handleConfig(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { // Parse request arguments var req ConfigRequest if err := request.BindArguments(&req); err != nil { return mcp.NewToolResultErrorf("Failed to parse request arguments: %v", err), nil } // Create a new config handler handler := &config.Handler{} // Convert request to AddConfigRequest addConfigReq := &schemav1.AddConfigRequest{ CustomAnalyzers: make([]*schemav1.CustomAnalyzer, 0), } // Add custom analyzers if present if len(req.CustomAnalyzers) > 0 { for _, ca := range req.CustomAnalyzers { addConfigReq.CustomAnalyzers = append(addConfigReq.CustomAnalyzers, &schemav1.CustomAnalyzer{ Name: ca.Name, Connection: &schemav1.Connection{ Url: ca.Connection.Url, Port: fmt.Sprintf("%d", ca.Connection.Port), }, }) } } // Add cache configuration if present if req.Cache.Type != "" { cacheConfig := &schemav1.Cache{} switch req.Cache.Type { case "s3": cacheConfig.CacheType = &schemav1.Cache_S3Cache{ S3Cache: &schemav1.S3Cache{ BucketName: req.Cache.BucketName, Region: req.Cache.Region, Endpoint: req.Cache.Endpoint, Insecure: req.Cache.Insecure, }, } case "azure": cacheConfig.CacheType = &schemav1.Cache_AzureCache{ AzureCache: &schemav1.AzureCache{ StorageAccount: req.Cache.StorageAccount, ContainerName: req.Cache.ContainerName, }, } case "gcs": cacheConfig.CacheType = &schemav1.Cache_GcsCache{ GcsCache: &schemav1.GCSCache{ BucketName: req.Cache.BucketName, Region: req.Cache.Region, ProjectId: req.Cache.ProjectId, }, } } addConfigReq.Cache = cacheConfig } // Apply the configuration using the shared function if err := handler.ApplyConfig(ctx, addConfigReq); err != nil { return mcp.NewToolResultErrorf("Failed to add config: %v", err), nil } return mcp.NewToolResultText("Successfully added configuration"), nil } // registerPrompts registers the prompts for the MCP server func (s *K8sGptMCPServer) registerPrompts() error { // Register troubleshooting prompts podTroubleshootPrompt := mcp.NewPrompt("troubleshoot-pod", mcp.WithPromptDescription("Guide for troubleshooting pod issues in Kubernetes"), mcp.WithArgument("podName"), mcp.WithArgument("namespace"), ) s.server.AddPrompt(podTroubleshootPrompt, s.getTroubleshootPodPrompt) deploymentTroubleshootPrompt := mcp.NewPrompt("troubleshoot-deployment", mcp.WithPromptDescription("Guide for troubleshooting deployment issues in Kubernetes"), mcp.WithArgument("deploymentName"), mcp.WithArgument("namespace"), ) s.server.AddPrompt(deploymentTroubleshootPrompt, s.getTroubleshootDeploymentPrompt) generalTroubleshootPrompt := mcp.NewPrompt("troubleshoot-cluster", mcp.WithPromptDescription("General guide for troubleshooting Kubernetes cluster issues"), ) s.server.AddPrompt(generalTroubleshootPrompt, s.getTroubleshootClusterPrompt) return nil } // registerResources registers the resources for the MCP server func (s *K8sGptMCPServer) registerResources() error { clusterInfoResource := mcp.NewResource("cluster-info", "cluster-info", mcp.WithResourceDescription("Get information about the Kubernetes cluster"), mcp.WithMIMEType("application/json"), ) s.server.AddResource(clusterInfoResource, s.getClusterInfo) namespacesResource := mcp.NewResource("namespaces", "namespaces", mcp.WithResourceDescription("List all namespaces in the cluster"), mcp.WithMIMEType("application/json"), ) s.server.AddResource(namespacesResource, s.getNamespacesResource) activeFiltersResource := mcp.NewResource("active-filters", "active-filters", mcp.WithResourceDescription("Get currently active analyzers/filters"), mcp.WithMIMEType("application/json"), ) s.server.AddResource(activeFiltersResource, s.getActiveFiltersResource) return nil } func (s *K8sGptMCPServer) getClusterInfo(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { // Create a new Kubernetes client client, err := kubernetes.NewClient("", "") if err != nil { return nil, fmt.Errorf("failed to create Kubernetes client: %v", err) } // Get cluster info from the client version, err := client.Client.Discovery().ServerVersion() if err != nil { return nil, fmt.Errorf("failed to get cluster version: %v", err) } data, err := json.Marshal(map[string]string{ "version": version.String(), "platform": version.Platform, "gitVersion": version.GitVersion, }) if err != nil { return []mcp.ResourceContents{ &mcp.TextResourceContents{ URI: "cluster-info", MIMEType: "text/plain", Text: "Failed to marshal cluster info", }, }, nil } return []mcp.ResourceContents{ &mcp.TextResourceContents{ URI: "cluster-info", MIMEType: "application/json", Text: string(data), }, }, nil } func (s *K8sGptMCPServer) getNamespacesResource(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { client, err := kubernetes.NewClient("", "") if err != nil { return nil, fmt.Errorf("failed to create Kubernetes client: %v", err) } namespaces, err := client.Client.CoreV1().Namespaces().List(ctx, metav1.ListOptions{}) if err != nil { return nil, fmt.Errorf("failed to list namespaces: %v", err) } // Extract just the namespace names names := make([]string, 0, len(namespaces.Items)) for _, ns := range namespaces.Items { names = append(names, ns.Name) } data, err := json.Marshal(map[string]interface{}{ "count": len(names), "namespaces": names, }) if err != nil { return []mcp.ResourceContents{ &mcp.TextResourceContents{ URI: "namespaces", MIMEType: "text/plain", Text: "Failed to marshal namespaces", }, }, nil } return []mcp.ResourceContents{ &mcp.TextResourceContents{ URI: "namespaces", MIMEType: "application/json", Text: string(data), }, }, nil } func (s *K8sGptMCPServer) getActiveFiltersResource(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { activeFilters := viper.GetStringSlice("active_filters") data, err := json.Marshal(map[string]interface{}{ "activeFilters": activeFilters, "count": len(activeFilters), }) if err != nil { return []mcp.ResourceContents{ &mcp.TextResourceContents{ URI: "active-filters", MIMEType: "text/plain", Text: "Failed to marshal active filters", }, }, nil } return []mcp.ResourceContents{ &mcp.TextResourceContents{ URI: "active-filters", MIMEType: "application/json", Text: string(data), }, }, nil } // Close closes the MCP server and releases resources func (s *K8sGptMCPServer) Close() error { return nil } // zapLoggerAdapter adapts zap.Logger to the interface expected by mark3labs/mcp-go type zapLoggerAdapter struct { logger *zap.Logger } func (z *zapLoggerAdapter) Infof(format string, v ...any) { z.logger.Info(fmt.Sprintf(format, v...)) } func (z *zapLoggerAdapter) Errorf(format string, v ...any) { z.logger.Error(fmt.Sprintf(format, v...)) } // stripANSI removes ANSI escape sequences from a string func stripANSI(input string) string { re := regexp.MustCompile(`\x1b\[[0-9;]*m`) return re.ReplaceAllString(input, "") } ================================================ FILE: pkg/server/mcp_handlers.go ================================================ /* Copyright 2024 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "context" "encoding/json" "fmt" "io" "strings" "github.com/k8sgpt-ai/k8sgpt/pkg/analyzer" "github.com/k8sgpt-ai/k8sgpt/pkg/integration" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" "github.com/mark3labs/mcp-go/mcp" "github.com/spf13/viper" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) const ( // DefaultListLimit is the default maximum number of resources to return DefaultListLimit = 100 // MaxListLimit is the maximum allowed limit for list operations MaxListLimit = 1000 ) // resourceLister defines a function that lists Kubernetes resources type resourceLister func(ctx context.Context, client *kubernetes.Client, namespace string, opts metav1.ListOptions) (interface{}, error) // resourceGetter defines a function that gets a single Kubernetes resource type resourceGetter func(ctx context.Context, client *kubernetes.Client, namespace, name string) (interface{}, error) // resourceRegistry maps resource types to their list and get functions var resourceRegistry = map[string]struct { list resourceLister get resourceGetter }{ "pod": { list: func(ctx context.Context, client *kubernetes.Client, namespace string, opts metav1.ListOptions) (interface{}, error) { return client.Client.CoreV1().Pods(namespace).List(ctx, opts) }, get: func(ctx context.Context, client *kubernetes.Client, namespace, name string) (interface{}, error) { return client.Client.CoreV1().Pods(namespace).Get(ctx, name, metav1.GetOptions{}) }, }, "deployment": { list: func(ctx context.Context, client *kubernetes.Client, namespace string, opts metav1.ListOptions) (interface{}, error) { return client.Client.AppsV1().Deployments(namespace).List(ctx, opts) }, get: func(ctx context.Context, client *kubernetes.Client, namespace, name string) (interface{}, error) { return client.Client.AppsV1().Deployments(namespace).Get(ctx, name, metav1.GetOptions{}) }, }, "service": { list: func(ctx context.Context, client *kubernetes.Client, namespace string, opts metav1.ListOptions) (interface{}, error) { return client.Client.CoreV1().Services(namespace).List(ctx, opts) }, get: func(ctx context.Context, client *kubernetes.Client, namespace, name string) (interface{}, error) { return client.Client.CoreV1().Services(namespace).Get(ctx, name, metav1.GetOptions{}) }, }, "node": { list: func(ctx context.Context, client *kubernetes.Client, namespace string, opts metav1.ListOptions) (interface{}, error) { return client.Client.CoreV1().Nodes().List(ctx, opts) }, get: func(ctx context.Context, client *kubernetes.Client, namespace, name string) (interface{}, error) { return client.Client.CoreV1().Nodes().Get(ctx, name, metav1.GetOptions{}) }, }, "job": { list: func(ctx context.Context, client *kubernetes.Client, namespace string, opts metav1.ListOptions) (interface{}, error) { return client.Client.BatchV1().Jobs(namespace).List(ctx, opts) }, get: func(ctx context.Context, client *kubernetes.Client, namespace, name string) (interface{}, error) { return client.Client.BatchV1().Jobs(namespace).Get(ctx, name, metav1.GetOptions{}) }, }, "cronjob": { list: func(ctx context.Context, client *kubernetes.Client, namespace string, opts metav1.ListOptions) (interface{}, error) { return client.Client.BatchV1().CronJobs(namespace).List(ctx, opts) }, get: func(ctx context.Context, client *kubernetes.Client, namespace, name string) (interface{}, error) { return client.Client.BatchV1().CronJobs(namespace).Get(ctx, name, metav1.GetOptions{}) }, }, "statefulset": { list: func(ctx context.Context, client *kubernetes.Client, namespace string, opts metav1.ListOptions) (interface{}, error) { return client.Client.AppsV1().StatefulSets(namespace).List(ctx, opts) }, get: func(ctx context.Context, client *kubernetes.Client, namespace, name string) (interface{}, error) { return client.Client.AppsV1().StatefulSets(namespace).Get(ctx, name, metav1.GetOptions{}) }, }, "daemonset": { list: func(ctx context.Context, client *kubernetes.Client, namespace string, opts metav1.ListOptions) (interface{}, error) { return client.Client.AppsV1().DaemonSets(namespace).List(ctx, opts) }, get: func(ctx context.Context, client *kubernetes.Client, namespace, name string) (interface{}, error) { return client.Client.AppsV1().DaemonSets(namespace).Get(ctx, name, metav1.GetOptions{}) }, }, "replicaset": { list: func(ctx context.Context, client *kubernetes.Client, namespace string, opts metav1.ListOptions) (interface{}, error) { return client.Client.AppsV1().ReplicaSets(namespace).List(ctx, opts) }, get: func(ctx context.Context, client *kubernetes.Client, namespace, name string) (interface{}, error) { return client.Client.AppsV1().ReplicaSets(namespace).Get(ctx, name, metav1.GetOptions{}) }, }, "configmap": { list: func(ctx context.Context, client *kubernetes.Client, namespace string, opts metav1.ListOptions) (interface{}, error) { return client.Client.CoreV1().ConfigMaps(namespace).List(ctx, opts) }, get: func(ctx context.Context, client *kubernetes.Client, namespace, name string) (interface{}, error) { return client.Client.CoreV1().ConfigMaps(namespace).Get(ctx, name, metav1.GetOptions{}) }, }, "secret": { list: func(ctx context.Context, client *kubernetes.Client, namespace string, opts metav1.ListOptions) (interface{}, error) { return client.Client.CoreV1().Secrets(namespace).List(ctx, opts) }, get: func(ctx context.Context, client *kubernetes.Client, namespace, name string) (interface{}, error) { return client.Client.CoreV1().Secrets(namespace).Get(ctx, name, metav1.GetOptions{}) }, }, "ingress": { list: func(ctx context.Context, client *kubernetes.Client, namespace string, opts metav1.ListOptions) (interface{}, error) { return client.Client.NetworkingV1().Ingresses(namespace).List(ctx, opts) }, get: func(ctx context.Context, client *kubernetes.Client, namespace, name string) (interface{}, error) { return client.Client.NetworkingV1().Ingresses(namespace).Get(ctx, name, metav1.GetOptions{}) }, }, "persistentvolumeclaim": { list: func(ctx context.Context, client *kubernetes.Client, namespace string, opts metav1.ListOptions) (interface{}, error) { return client.Client.CoreV1().PersistentVolumeClaims(namespace).List(ctx, opts) }, get: func(ctx context.Context, client *kubernetes.Client, namespace, name string) (interface{}, error) { return client.Client.CoreV1().PersistentVolumeClaims(namespace).Get(ctx, name, metav1.GetOptions{}) }, }, "persistentvolume": { list: func(ctx context.Context, client *kubernetes.Client, namespace string, opts metav1.ListOptions) (interface{}, error) { return client.Client.CoreV1().PersistentVolumes().List(ctx, opts) }, get: func(ctx context.Context, client *kubernetes.Client, namespace, name string) (interface{}, error) { return client.Client.CoreV1().PersistentVolumes().Get(ctx, name, metav1.GetOptions{}) }, }, } // Resource type aliases for convenience var resourceTypeAliases = map[string]string{ "pods": "pod", "deployments": "deployment", "services": "service", "svc": "service", "nodes": "node", "jobs": "job", "cronjobs": "cronjob", "statefulsets": "statefulset", "sts": "statefulset", "daemonsets": "daemonset", "ds": "daemonset", "replicasets": "replicaset", "rs": "replicaset", "configmaps": "configmap", "cm": "configmap", "secrets": "secret", "ingresses": "ingress", "ing": "ingress", "persistentvolumeclaims": "persistentvolumeclaim", "pvc": "persistentvolumeclaim", "persistentvolumes": "persistentvolume", "pv": "persistentvolume", } // normalizeResourceType converts resource type variants to canonical form func normalizeResourceType(resourceType string) (string, error) { normalized := strings.ToLower(resourceType) // Check if it's an alias if canonical, ok := resourceTypeAliases[normalized]; ok { normalized = canonical } // Check if it's a known resource type if _, ok := resourceRegistry[normalized]; !ok { return "", fmt.Errorf("unsupported resource type: %s", resourceType) } return normalized, nil } // marshalJSON marshals data to JSON with proper error handling func marshalJSON(data interface{}) (string, error) { jsonData, err := json.MarshalIndent(data, "", " ") if err != nil { return "", fmt.Errorf("failed to marshal JSON: %w", err) } return string(jsonData), nil } // handleListResources lists Kubernetes resources of a specific type func (s *K8sGptMCPServer) handleListResources(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { var req struct { ResourceType string `json:"resourceType"` Namespace string `json:"namespace,omitempty"` LabelSelector string `json:"labelSelector,omitempty"` Limit int64 `json:"limit,omitempty"` } if err := request.BindArguments(&req); err != nil { return mcp.NewToolResultErrorf("Failed to parse request arguments: %v", err), nil } if req.ResourceType == "" { return mcp.NewToolResultErrorf("resourceType is required"), nil } // Normalize and validate resource type resourceType, err := normalizeResourceType(req.ResourceType) if err != nil { supportedTypes := make([]string, 0, len(resourceRegistry)) for key := range resourceRegistry { supportedTypes = append(supportedTypes, key) } return mcp.NewToolResultErrorf("%v. Supported types: %v", err, supportedTypes), nil } // Set default and validate limit if req.Limit == 0 { req.Limit = DefaultListLimit } else if req.Limit > MaxListLimit { req.Limit = MaxListLimit } client, err := kubernetes.NewClient("", "") if err != nil { return mcp.NewToolResultErrorf("Failed to create Kubernetes client: %v", err), nil } listOptions := metav1.ListOptions{ LabelSelector: req.LabelSelector, Limit: req.Limit, } // Get the list function from registry listFunc := resourceRegistry[resourceType].list result, err := listFunc(ctx, client, req.Namespace, listOptions) if err != nil { return mcp.NewToolResultErrorf("Failed to list %s: %v", resourceType, err), nil } // Extract items from the result (all list types have an Items field) resultJSON, err := marshalJSON(result) if err != nil { return mcp.NewToolResultErrorf("Failed to serialize result: %v", err), nil } return mcp.NewToolResultText(resultJSON), nil } // handleGetResource gets detailed information about a specific resource func (s *K8sGptMCPServer) handleGetResource(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { var req struct { ResourceType string `json:"resourceType"` Name string `json:"name"` Namespace string `json:"namespace,omitempty"` } if err := request.BindArguments(&req); err != nil { return mcp.NewToolResultErrorf("Failed to parse request arguments: %v", err), nil } if req.ResourceType == "" { return mcp.NewToolResultErrorf("resourceType is required"), nil } if req.Name == "" { return mcp.NewToolResultErrorf("name is required"), nil } // Normalize and validate resource type resourceType, err := normalizeResourceType(req.ResourceType) if err != nil { return mcp.NewToolResultErrorf("%v", err), nil } client, err := kubernetes.NewClient("", "") if err != nil { return mcp.NewToolResultErrorf("Failed to create Kubernetes client: %v", err), nil } // Get the get function from registry getFunc := resourceRegistry[resourceType].get result, err := getFunc(ctx, client, req.Namespace, req.Name) if err != nil { return mcp.NewToolResultErrorf("Failed to get %s '%s': %v", resourceType, req.Name, err), nil } resultJSON, err := marshalJSON(result) if err != nil { return mcp.NewToolResultErrorf("Failed to serialize result: %v", err), nil } return mcp.NewToolResultText(resultJSON), nil } // handleListNamespaces lists all namespaces in the cluster func (s *K8sGptMCPServer) handleListNamespaces(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { client, err := kubernetes.NewClient("", "") if err != nil { return mcp.NewToolResultErrorf("Failed to create Kubernetes client: %v", err), nil } namespaces, err := client.Client.CoreV1().Namespaces().List(ctx, metav1.ListOptions{}) if err != nil { return mcp.NewToolResultErrorf("Failed to list namespaces: %v", err), nil } resultJSON, err := marshalJSON(namespaces.Items) if err != nil { return mcp.NewToolResultErrorf("Failed to serialize result: %v", err), nil } return mcp.NewToolResultText(resultJSON), nil } // handleListEvents lists Kubernetes events func (s *K8sGptMCPServer) handleListEvents(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { var req struct { Namespace string `json:"namespace,omitempty"` InvolvedObjectName string `json:"involvedObjectName,omitempty"` InvolvedObjectKind string `json:"involvedObjectKind,omitempty"` Limit int64 `json:"limit,omitempty"` } if err := request.BindArguments(&req); err != nil { return mcp.NewToolResultErrorf("Failed to parse request arguments: %v", err), nil } if req.Limit == 0 { req.Limit = DefaultListLimit } else if req.Limit > MaxListLimit { req.Limit = MaxListLimit } client, err := kubernetes.NewClient("", "") if err != nil { return mcp.NewToolResultErrorf("Failed to create Kubernetes client: %v", err), nil } listOptions := metav1.ListOptions{ Limit: req.Limit, } events, err := client.Client.CoreV1().Events(req.Namespace).List(ctx, listOptions) if err != nil { return mcp.NewToolResultErrorf("Failed to list events: %v", err), nil } // Filter events if needed filteredEvents := []corev1.Event{} for _, event := range events.Items { if req.InvolvedObjectName != "" && event.InvolvedObject.Name != req.InvolvedObjectName { continue } if req.InvolvedObjectKind != "" && event.InvolvedObject.Kind != req.InvolvedObjectKind { continue } filteredEvents = append(filteredEvents, event) } resultJSON, err := marshalJSON(filteredEvents) if err != nil { return mcp.NewToolResultErrorf("Failed to serialize result: %v", err), nil } return mcp.NewToolResultText(resultJSON), nil } // handleGetLogs retrieves logs from a pod container func (s *K8sGptMCPServer) handleGetLogs(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { var req struct { PodName string `json:"podName"` Namespace string `json:"namespace"` Container string `json:"container,omitempty"` Previous bool `json:"previous,omitempty"` TailLines int64 `json:"tailLines,omitempty"` SinceSeconds int64 `json:"sinceSeconds,omitempty"` } if err := request.BindArguments(&req); err != nil { return mcp.NewToolResultErrorf("Failed to parse request arguments: %v", err), nil } if req.PodName == "" { return mcp.NewToolResultErrorf("podName is required"), nil } if req.Namespace == "" { return mcp.NewToolResultErrorf("namespace is required"), nil } if req.TailLines == 0 { req.TailLines = 100 } client, err := kubernetes.NewClient("", "") if err != nil { return mcp.NewToolResultErrorf("Failed to create Kubernetes client: %v", err), nil } podLogOpts := &corev1.PodLogOptions{ Container: req.Container, Previous: req.Previous, TailLines: &req.TailLines, } if req.SinceSeconds > 0 { podLogOpts.SinceSeconds = &req.SinceSeconds } logRequest := client.Client.CoreV1().Pods(req.Namespace).GetLogs(req.PodName, podLogOpts) logStream, err := logRequest.Stream(ctx) if err != nil { return mcp.NewToolResultErrorf("Failed to get logs: %v", err), nil } defer func() { _ = logStream.Close() }() logs, err := io.ReadAll(logStream) if err != nil { return mcp.NewToolResultErrorf("Failed to read logs: %v", err), nil } return mcp.NewToolResultText(string(logs)), nil } // handleListFilters lists available and active filters func (s *K8sGptMCPServer) handleListFilters(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { coreFilters, additionalFilters, integrationFilters := analyzer.ListFilters() active := viper.GetStringSlice("active_filters") result := map[string]interface{}{ "coreFilters": coreFilters, "additionalFilters": additionalFilters, "integrationFilters": integrationFilters, "activeFilters": active, } resultJSON, err := marshalJSON(result) if err != nil { return mcp.NewToolResultErrorf("Failed to serialize result: %v", err), nil } return mcp.NewToolResultText(resultJSON), nil } // handleAddFilters adds filters to enable specific analyzers func (s *K8sGptMCPServer) handleAddFilters(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { var req struct { Filters []string `json:"filters"` } if err := request.BindArguments(&req); err != nil { return mcp.NewToolResultErrorf("Failed to parse request arguments: %v", err), nil } if len(req.Filters) == 0 { return mcp.NewToolResultErrorf("filters array is required and cannot be empty"), nil } activeFilters := viper.GetStringSlice("active_filters") addedFilters := []string{} for _, filter := range req.Filters { if !contains(activeFilters, filter) { activeFilters = append(activeFilters, filter) addedFilters = append(addedFilters, filter) } } viper.Set("active_filters", activeFilters) if err := viper.WriteConfig(); err != nil { return mcp.NewToolResultErrorf("Failed to save configuration: %v", err), nil } if len(addedFilters) == 0 { return mcp.NewToolResultText("All specified filters were already active"), nil } return mcp.NewToolResultText(fmt.Sprintf("Successfully added filters: %v", addedFilters)), nil } // handleRemoveFilters removes filters to disable specific analyzers func (s *K8sGptMCPServer) handleRemoveFilters(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { var req struct { Filters []string `json:"filters"` } if err := request.BindArguments(&req); err != nil { return mcp.NewToolResultErrorf("Failed to parse request arguments: %v", err), nil } if len(req.Filters) == 0 { return mcp.NewToolResultErrorf("filters array is required and cannot be empty"), nil } activeFilters := viper.GetStringSlice("active_filters") newFilters := []string{} removedFilters := []string{} for _, filter := range activeFilters { if !contains(req.Filters, filter) { newFilters = append(newFilters, filter) } else { removedFilters = append(removedFilters, filter) } } viper.Set("active_filters", newFilters) if err := viper.WriteConfig(); err != nil { return mcp.NewToolResultErrorf("Failed to save configuration: %v", err), nil } if len(removedFilters) == 0 { return mcp.NewToolResultText("None of the specified filters were active"), nil } return mcp.NewToolResultText(fmt.Sprintf("Successfully removed filters: %v", removedFilters)), nil } // handleListIntegrations lists available integrations func (s *K8sGptMCPServer) handleListIntegrations(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { integrationProvider := integration.NewIntegration() integrations := integrationProvider.List() result := []map[string]interface{}{} for _, integ := range integrations { active, _ := integrationProvider.IsActivate(integ) result = append(result, map[string]interface{}{ "name": integ, "active": active, }) } resultJSON, err := marshalJSON(result) if err != nil { return mcp.NewToolResultErrorf("Failed to serialize result: %v", err), nil } return mcp.NewToolResultText(resultJSON), nil } // contains checks if a string slice contains a specific string func contains(slice []string, item string) bool { for _, s := range slice { if s == item { return true } } return false } ================================================ FILE: pkg/server/mcp_prompts.go ================================================ /* Copyright 2024 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "context" "fmt" "github.com/mark3labs/mcp-go/mcp" ) // getTroubleshootPodPrompt returns a prompt for pod troubleshooting func (s *K8sGptMCPServer) getTroubleshootPodPrompt(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { podName := "" namespace := "" if request.Params.Arguments != nil { podName = request.Params.Arguments["podName"] namespace = request.Params.Arguments["namespace"] } promptText := fmt.Sprintf(`You are troubleshooting a Kubernetes pod issue. Pod: %s Namespace: %s Troubleshooting steps: 1. Use 'get-resource' tool to get pod details and check status, conditions, and events 2. Use 'list-events' tool with the pod name to see recent events 3. Use 'get-logs' tool to check container logs for errors 4. Check if the pod has multiple containers and inspect each 5. If the pod is in CrashLoopBackOff, use 'get-logs' with previous=true 6. Use 'analyze' tool with filters=['Pod'] to get AI-powered analysis 7. Check related resources like ConfigMaps, Secrets, and PVCs Common issues to check: - Image pull errors (check imagePullSecrets) - Resource limits (CPU/memory) - Liveness/readiness probe failures - Volume mount issues - Environment variable problems - Network connectivity issues`, podName, namespace) return &mcp.GetPromptResult{ Description: "Pod troubleshooting guide", Messages: []mcp.PromptMessage{ { Role: "user", Content: mcp.TextContent{ Type: "text", Text: promptText, }, }, }, }, nil } // getTroubleshootDeploymentPrompt returns a prompt for deployment troubleshooting func (s *K8sGptMCPServer) getTroubleshootDeploymentPrompt(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { deploymentName := "" namespace := "" if request.Params.Arguments != nil { deploymentName = request.Params.Arguments["deploymentName"] namespace = request.Params.Arguments["namespace"] } promptText := fmt.Sprintf(`You are troubleshooting a Kubernetes deployment issue. Deployment: %s Namespace: %s Troubleshooting steps: 1. Use 'get-resource' tool to get deployment details and check replica status 2. Use 'list-resources' with resourceType='replicasets' to check ReplicaSets 3. Use 'list-resources' with resourceType='pods' and labelSelector to find deployment pods 4. Use 'list-events' tool to see deployment-related events 5. Use 'analyze' tool with filters=['Deployment','Pod'] for comprehensive analysis 6. Check pod status and logs for individual pod issues 7. Verify image availability and pull secrets 8. Check resource quotas and limits Common deployment issues: - Insufficient resources in the cluster - Image pull failures - Invalid configuration (ConfigMaps/Secrets) - Failed rolling updates - Readiness probe failures preventing rollout - PVC binding issues - Node selector/affinity constraints`, deploymentName, namespace) return &mcp.GetPromptResult{ Description: "Deployment troubleshooting guide", Messages: []mcp.PromptMessage{ { Role: "user", Content: mcp.TextContent{ Type: "text", Text: promptText, }, }, }, }, nil } // getTroubleshootClusterPrompt returns a prompt for general cluster troubleshooting func (s *K8sGptMCPServer) getTroubleshootClusterPrompt(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { promptText := `You are performing a general Kubernetes cluster health check and troubleshooting. Recommended troubleshooting workflow: 1. CLUSTER OVERVIEW: - Use 'cluster-info' to get cluster version - Use 'list-namespaces' to see all namespaces - Use 'list-resources' with resourceType='nodes' to check node health 2. RESOURCE ANALYSIS: - Use 'analyze' tool with explain=true for comprehensive AI-powered analysis - Start with core resources: filters=['Pod','Deployment','Service'] - Add more filters as needed: ['Node','PersistentVolumeClaim','Job','CronJob'] 3. EVENT INSPECTION: - Use 'list-events' to see recent cluster events - Filter by namespace for focused troubleshooting - Look for Warning and Error events 4. SPECIFIC RESOURCE INVESTIGATION: - Use 'list-resources' to find problematic resources - Use 'get-resource' for detailed inspection - Use 'get-logs' to examine container logs 5. CONFIGURATION CHECK: - Use 'list-filters' to see available analyzers - Use 'list-integrations' to check integrations (Prometheus, AWS, etc.) - Use 'config' tool to modify settings if needed Common cluster-wide issues: - Node pressure (CPU, memory, disk) - Network policies blocking traffic - Storage provisioning problems - RBAC permission issues - Certificate expiration - Control plane component failures - Resource quota exhaustion - DNS resolution problems Use the available tools systematically to narrow down the issue.` return &mcp.GetPromptResult{ Description: "General cluster troubleshooting guide", Messages: []mcp.PromptMessage{ { Role: "user", Content: mcp.TextContent{ Type: "text", Text: promptText, }, }, }, }, nil } ================================================ FILE: pkg/server/query/handler.go ================================================ package query import rpc "buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go/schema/v1/schemav1grpc" type Handler struct { rpc.UnimplementedServerQueryServiceServer } ================================================ FILE: pkg/server/query/query.go ================================================ package query import ( "context" "fmt" schemav1 "buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go/schema/v1" "github.com/k8sgpt-ai/k8sgpt/pkg/ai" ) func (h *Handler) Query(ctx context.Context, i *schemav1.QueryRequest) ( *schemav1.QueryResponse, error, ) { // Create client factory and config provider factory := ai.GetAIClientFactory() configProvider := ai.GetConfigProvider() // Use the factory to create the client aiClient := factory.NewClient(i.Backend) defer aiClient.Close() var configAI ai.AIConfiguration if err := configProvider.UnmarshalKey("ai", &configAI); err != nil { return &schemav1.QueryResponse{ Response: "", Error: &schemav1.QueryError{ Message: fmt.Sprintf("Failed to unmarshal AI configuration: %v", err), }, }, nil } var aiProvider ai.AIProvider for _, provider := range configAI.Providers { if i.Backend == provider.Name { aiProvider = provider break } } if aiProvider.Name == "" { return &schemav1.QueryResponse{ Response: "", Error: &schemav1.QueryError{ Message: fmt.Sprintf("AI provider %s not found in configuration", i.Backend), }, }, nil } // Configure the AI client if err := aiClient.Configure(&aiProvider); err != nil { return &schemav1.QueryResponse{ Response: "", Error: &schemav1.QueryError{ Message: fmt.Sprintf("Failed to configure AI client: %v", err), }, }, nil } resp, err := aiClient.GetCompletion(ctx, i.Query) var errMessage string = "" if err != nil { errMessage = err.Error() } return &schemav1.QueryResponse{ Response: resp, Error: &schemav1.QueryError{ Message: errMessage, }, }, nil } ================================================ FILE: pkg/server/query/query_test.go ================================================ package query import ( "context" "errors" "testing" schemav1 "buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go/schema/v1" "github.com/k8sgpt-ai/k8sgpt/pkg/ai" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) // MockAI is a mock implementation of the ai.IAI interface for testing type MockAI struct { mock.Mock } func (m *MockAI) Configure(config ai.IAIConfig) error { args := m.Called(config) return args.Error(0) } func (m *MockAI) GetCompletion(ctx context.Context, prompt string) (string, error) { args := m.Called(ctx, prompt) return args.String(0), args.Error(1) } func (m *MockAI) GetName() string { args := m.Called() return args.String(0) } func (m *MockAI) Close() { m.Called() } // MockAIClientFactory is a mock implementation of AIClientFactory type MockAIClientFactory struct { mock.Mock } func (m *MockAIClientFactory) NewClient(provider string) ai.IAI { args := m.Called(provider) return args.Get(0).(ai.IAI) } // MockConfigProvider is a mock implementation of ConfigProvider type MockConfigProvider struct { mock.Mock } func (m *MockConfigProvider) UnmarshalKey(key string, rawVal interface{}) error { args := m.Called(key, rawVal) // If we want to set the rawVal (which is a pointer) if fn, ok := args.Get(0).(func(interface{})); ok && fn != nil { fn(rawVal) } // Return the error as the first return value return args.Error(0) } func TestQuery_Success(t *testing.T) { // Setup mocks mockAI := new(MockAI) mockFactory := new(MockAIClientFactory) mockConfig := new(MockConfigProvider) // Set test implementations ai.SetTestAIClientFactory(mockFactory) ai.SetTestConfigProvider(mockConfig) defer ai.ResetTestImplementations() // Define test data testBackend := "test-backend" testQuery := "test query" testResponse := "test response" // Setup expectations mockFactory.On("NewClient", testBackend).Return(mockAI) mockAI.On("Close").Return() // Set up configuration with a valid provider mockConfig.On("UnmarshalKey", "ai", mock.Anything).Run(func(args mock.Arguments) { config := args.Get(1).(*ai.AIConfiguration) *config = ai.AIConfiguration{ Providers: []ai.AIProvider{ { Name: testBackend, Password: "test-password", Model: "test-model", }, }, } }).Return(nil) mockAI.On("Configure", mock.AnythingOfType("*ai.AIProvider")).Return(nil) mockAI.On("GetCompletion", mock.Anything, testQuery).Return(testResponse, nil) // Create handler and call Query handler := &Handler{} response, err := handler.Query(context.Background(), &schemav1.QueryRequest{ Backend: testBackend, Query: testQuery, }) // Assertions assert.NoError(t, err) assert.NotNil(t, response) assert.Equal(t, testResponse, response.Response) assert.Equal(t, "", response.Error.Message) // Verify mocks mockAI.AssertExpectations(t) mockFactory.AssertExpectations(t) mockConfig.AssertExpectations(t) } func TestQuery_UnmarshalError(t *testing.T) { // Setup mocks mockAI := new(MockAI) mockFactory := new(MockAIClientFactory) mockConfig := new(MockConfigProvider) // Set test implementations ai.SetTestAIClientFactory(mockFactory) ai.SetTestConfigProvider(mockConfig) defer ai.ResetTestImplementations() // Setup expectations mockFactory.On("NewClient", "test-backend").Return(mockAI) mockAI.On("Close").Return() // Mock unmarshal error mockConfig.On("UnmarshalKey", "ai", mock.Anything).Return(errors.New("unmarshal error")) // Create handler and call Query handler := &Handler{} response, err := handler.Query(context.Background(), &schemav1.QueryRequest{ Backend: "test-backend", Query: "test query", }) // Assertions assert.NoError(t, err) assert.NotNil(t, response) assert.Equal(t, "", response.Response) assert.Contains(t, response.Error.Message, "Failed to unmarshal AI configuration") // Verify mocks mockAI.AssertExpectations(t) mockFactory.AssertExpectations(t) mockConfig.AssertExpectations(t) } func TestQuery_ProviderNotFound(t *testing.T) { // Setup mocks mockAI := new(MockAI) mockFactory := new(MockAIClientFactory) mockConfig := new(MockConfigProvider) // Set test implementations ai.SetTestAIClientFactory(mockFactory) ai.SetTestConfigProvider(mockConfig) defer ai.ResetTestImplementations() // Define test data testBackend := "test-backend" // Setup expectations mockFactory.On("NewClient", testBackend).Return(mockAI) mockAI.On("Close").Return() // Set up configuration with no matching provider mockConfig.On("UnmarshalKey", "ai", mock.Anything).Run(func(args mock.Arguments) { config := args.Get(1).(*ai.AIConfiguration) *config = ai.AIConfiguration{ Providers: []ai.AIProvider{ { Name: "other-backend", }, }, } }).Return(nil) // Create handler and call Query handler := &Handler{} response, err := handler.Query(context.Background(), &schemav1.QueryRequest{ Backend: testBackend, Query: "test query", }) // Assertions assert.NoError(t, err) assert.NotNil(t, response) assert.Equal(t, "", response.Response) assert.Contains(t, response.Error.Message, "AI provider test-backend not found in configuration") // Verify mocks mockAI.AssertExpectations(t) mockFactory.AssertExpectations(t) mockConfig.AssertExpectations(t) } func TestQuery_ConfigureError(t *testing.T) { // Setup mocks mockAI := new(MockAI) mockFactory := new(MockAIClientFactory) mockConfig := new(MockConfigProvider) // Set test implementations ai.SetTestAIClientFactory(mockFactory) ai.SetTestConfigProvider(mockConfig) defer ai.ResetTestImplementations() // Define test data testBackend := "test-backend" // Setup expectations mockFactory.On("NewClient", testBackend).Return(mockAI) mockAI.On("Close").Return() // Set up configuration with a valid provider mockConfig.On("UnmarshalKey", "ai", mock.Anything).Run(func(args mock.Arguments) { config := args.Get(1).(*ai.AIConfiguration) *config = ai.AIConfiguration{ Providers: []ai.AIProvider{ { Name: testBackend, }, }, } }).Return(nil) // Mock configure error mockAI.On("Configure", mock.AnythingOfType("*ai.AIProvider")).Return(errors.New("configure error")) // Create handler and call Query handler := &Handler{} response, err := handler.Query(context.Background(), &schemav1.QueryRequest{ Backend: testBackend, Query: "test query", }) // Assertions assert.NoError(t, err) assert.NotNil(t, response) assert.Equal(t, "", response.Response) assert.Contains(t, response.Error.Message, "Failed to configure AI client") // Verify mocks mockAI.AssertExpectations(t) mockFactory.AssertExpectations(t) mockConfig.AssertExpectations(t) } func TestQuery_GetCompletionError(t *testing.T) { // Setup mocks mockAI := new(MockAI) mockFactory := new(MockAIClientFactory) mockConfig := new(MockConfigProvider) // Set test implementations ai.SetTestAIClientFactory(mockFactory) ai.SetTestConfigProvider(mockConfig) defer ai.ResetTestImplementations() // Define test data testBackend := "test-backend" testQuery := "test query" // Setup expectations mockFactory.On("NewClient", testBackend).Return(mockAI) mockAI.On("Close").Return() // Set up configuration with a valid provider mockConfig.On("UnmarshalKey", "ai", mock.Anything).Run(func(args mock.Arguments) { config := args.Get(1).(*ai.AIConfiguration) *config = ai.AIConfiguration{ Providers: []ai.AIProvider{ { Name: testBackend, }, }, } }).Return(nil) mockAI.On("Configure", mock.AnythingOfType("*ai.AIProvider")).Return(nil) mockAI.On("GetCompletion", mock.Anything, testQuery).Return("", errors.New("completion error")) // Create handler and call Query handler := &Handler{} response, err := handler.Query(context.Background(), &schemav1.QueryRequest{ Backend: testBackend, Query: testQuery, }) // Assertions assert.NoError(t, err) assert.NotNil(t, response) assert.Equal(t, "", response.Response) assert.Equal(t, "completion error", response.Error.Message) // Verify mocks mockAI.AssertExpectations(t) mockFactory.AssertExpectations(t) mockConfig.AssertExpectations(t) } ================================================ FILE: pkg/server/server.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "context" "errors" "fmt" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "github.com/k8sgpt-ai/k8sgpt/pkg/server/analyze" "github.com/k8sgpt-ai/k8sgpt/pkg/server/config" "github.com/k8sgpt-ai/k8sgpt/pkg/server/query" "log" "net" "net/http" "strings" "time" //nolint:staticcheck // Ignoring SA1019 for compatibility reasons gw2 "buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc-ecosystem/gateway/v2/schema/v1/server_analyzer_service/schemav1gateway" //nolint:staticcheck // Ignoring SA1019 for compatibility reasons gw "buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc-ecosystem/gateway/v2/schema/v1/server_config_service/schemav1gateway" rpc "buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go/schema/v1/schemav1grpc" "github.com/go-logr/zapr" "github.com/prometheus/client_golang/prometheus/promhttp" "go.uber.org/zap" "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" ctrl "sigs.k8s.io/controller-runtime" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/reflection" ) type Config struct { Port string MetricsPort string Backend string Key string Token string Output string ConfigHandler *config.Handler AnalyzeHandler *analyze.Handler QueryHandler *query.Handler Logger *zap.Logger // Filters can be injected into the server to limit analysis to specific analyzers Filters []string metricsServer *http.Server listener net.Listener EnableHttp bool } type Health struct { Status string `json:"status"` Success int `json:"success"` Failure int `json:"failure"` } //nolint:unused var health = Health{ Status: "ok", Success: 0, Failure: 0, } func (s *Config) Shutdown() error { return s.listener.Close() } // grpcHandlerFunc returns an http.Handler that delegates to grpcServer on incoming gRPC // connections or otherHandler otherwise. func grpcHandlerFunc(grpcServer *grpc.Server, otherHandler http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.ProtoMajor == 2 && strings.Contains(r.Header.Get("Content-Type"), "application/grpc") { grpcServer.ServeHTTP(w, r) } else { otherHandler.ServeHTTP(w, r) } }) } func (s *Config) Serve() error { ctrl.SetLogger(zapr.NewLogger(s.Logger)) var lis net.Listener var err error address := fmt.Sprintf(":%s", s.Port) lis, err = net.Listen("tcp", address) if err != nil { return err } s.ConfigHandler = &config.Handler{} s.AnalyzeHandler = &analyze.Handler{} s.QueryHandler = &query.Handler{} s.listener = lis s.Logger.Info(fmt.Sprintf("binding api to %s", s.Port)) grpcServerUnaryInterceptor := grpc.UnaryInterceptor(LogInterceptor(s.Logger)) grpcServer := grpc.NewServer(grpcServerUnaryInterceptor) reflection.Register(grpcServer) rpc.RegisterServerConfigServiceServer(grpcServer, s.ConfigHandler) rpc.RegisterServerAnalyzerServiceServer(grpcServer, s.AnalyzeHandler) rpc.RegisterServerQueryServiceServer(grpcServer, s.QueryHandler) if s.EnableHttp { s.Logger.Info("enabling rest/http api") gwmux := runtime.NewServeMux() err = gw.RegisterServerConfigServiceHandlerFromEndpoint(context.Background(), gwmux, fmt.Sprintf("localhost:%s", s.Port), []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}) if err != nil { log.Fatalln("Failed to register gateway:", err) } err = gw2.RegisterServerAnalyzerServiceHandlerFromEndpoint(context.Background(), gwmux, fmt.Sprintf("localhost:%s", s.Port), []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}) if err != nil { log.Fatalln("Failed to register gateway:", err) } srv := &http.Server{ Addr: address, Handler: h2c.NewHandler(grpcHandlerFunc(grpcServer, gwmux), &http2.Server{}), } if err := srv.Serve(lis); err != nil { return err } } else { if err := grpcServer.Serve( lis, ); err != nil && !errors.Is(err, http.ErrServerClosed) { return err } } return nil } func (s *Config) ServeMetrics() error { s.Logger.Info(fmt.Sprintf("binding metrics to %s", s.MetricsPort)) s.metricsServer = &http.Server{ ReadHeaderTimeout: 3 * time.Second, Addr: fmt.Sprintf(":%s", s.MetricsPort), } s.metricsServer.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/healthz": w.WriteHeader(http.StatusOK) case "/metrics": promhttp.Handler().ServeHTTP(w, r) default: w.WriteHeader(http.StatusNotFound) } }) if err := s.metricsServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { return err } return nil } ================================================ FILE: pkg/server/server_test.go ================================================ package server import ( "bytes" "context" "io" "net" "net/http" "testing" "time" "github.com/k8sgpt-ai/k8sgpt/pkg/ai" "github.com/stretchr/testify/assert" "go.uber.org/zap" "google.golang.org/grpc" "google.golang.org/grpc/reflection/grpc_reflection_v1alpha" ) func TestServe(t *testing.T) { logger, _ := zap.NewDevelopment() defer func() { err := logger.Sync() if err != nil { t.Logf("logger.Sync() error: %v", err) } }() s := &Config{ Port: "50059", Logger: logger, EnableHttp: false, } go func() { err := s.Serve() time.Sleep(time.Second * 2) assert.NoError(t, err, "Serve should not return an error") }() // Wait until the server is ready to accept connections err := waitForPort("localhost:50059", 10*time.Second) assert.NoError(t, err, "Server should start without error") conn, err := grpc.Dial("localhost:50059", grpc.WithInsecure()) assert.NoError(t, err, "Should be able to dial the server") defer func() { if err := conn.Close(); err != nil { t.Logf("failed to close connection: %v", err) } }() // Test a simple gRPC reflection request cli := grpc_reflection_v1alpha.NewServerReflectionClient(conn) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() resp, err := cli.ServerReflectionInfo(ctx) assert.NoError(t, err, "Should be able to get server reflection info") assert.NotNil(t, resp, "Response should not be nil") // Cleanup err = s.Shutdown() assert.NoError(t, err, "Shutdown should not return an error") } // TestMCPServerCreation tests the creation of an MCP server func TestMCPServerCreation(t *testing.T) { logger, _ := zap.NewDevelopment() defer func() { err := logger.Sync() if err != nil { t.Logf("logger.Sync() error: %v", err) } }() aiProvider := &ai.AIProvider{ Name: "test-provider", Password: "test-password", Model: "test-model", } // Test HTTP mode mcpServer, err := NewMCPServer("8088", aiProvider, true, logger) assert.NoError(t, err, "Should be able to create MCP server with HTTP transport") assert.NotNil(t, mcpServer, "MCP server should not be nil") assert.True(t, mcpServer.useHTTP, "MCP server should be in HTTP mode") assert.Equal(t, "8088", mcpServer.port, "Port should be set correctly") // Test stdio mode mcpServerStdio, err := NewMCPServer("8088", aiProvider, false, logger) assert.NoError(t, err, "Should be able to create MCP server with stdio transport") assert.NotNil(t, mcpServerStdio, "MCP server should not be nil") assert.False(t, mcpServerStdio.useHTTP, "MCP server should be in stdio mode") } // TestMCPServerBasicHTTP tests basic HTTP connectivity to the MCP server func TestMCPServerBasicHTTP(t *testing.T) { logger, _ := zap.NewDevelopment() defer func() { err := logger.Sync() if err != nil { t.Logf("logger.Sync() error: %v", err) } }() aiProvider := &ai.AIProvider{ Name: "test-provider", Password: "test-password", Model: "test-model", } mcpServer, err := NewMCPServer("8091", aiProvider, true, logger) assert.NoError(t, err, "Should be able to create MCP server") // For HTTP mode, the server is already started in NewMCPServer // No need to call Start() as it's already running in a goroutine // Wait for the server to start err = waitForPort("localhost:8091", 10*time.Second) if err != nil { t.Skipf("MCP server did not start within timeout: %v", err) } // First, initialize the session initRequest := `{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-03-26", "capabilities": { "tools": {}, "resources": {}, "prompts": {} }, "clientInfo": { "name": "test-client", "version": "1.0.0" } } }` initResp, err := http.Post("http://localhost:8091/mcp", "application/json", bytes.NewBufferString(initRequest)) if err != nil { t.Logf("Initialize request failed: %v", err) return } defer func() { if err := initResp.Body.Close(); err != nil { t.Logf("Error closing init response body: %v", err) } }() // Read initialization response initBody, err := io.ReadAll(initResp.Body) if err != nil { t.Logf("Failed to read init response body: %v", err) } else { t.Logf("Init response status: %d, body: %s", initResp.StatusCode, string(initBody)) } // Extract session ID from response headers if present sessionID := initResp.Header.Get("Mcp-Session-Id") if sessionID == "" { t.Logf("No session ID in response headers") } // Now test tools/list with session ID if available headers := map[string]string{ "Content-Type": "application/json", "Accept": "application/json,text/event-stream", } if sessionID != "" { headers["Mcp-Session-Id"] = sessionID } req, err := http.NewRequest("POST", "http://localhost:8091/mcp", bytes.NewBufferString(`{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}`)) if err != nil { t.Logf("Failed to create request: %v", err) return } for key, value := range headers { req.Header.Set(key, value) } client := &http.Client{} resp, err := client.Do(req) if err != nil { t.Logf("MCP endpoint test skipped (server might not be fully ready): %v", err) return } defer func() { err := resp.Body.Close() if err != nil { t.Logf("resp.Body.Close() error: %v", err) } }() // Read response body for debugging body, err := io.ReadAll(resp.Body) if err != nil { t.Logf("Failed to read response body: %v", err) } else { t.Logf("Response status: %d, body: %s", resp.StatusCode, string(body)) } // Accept both 200 and 404 as valid responses (404 means endpoint not implemented) if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNotFound { t.Errorf("MCP endpoint returned unexpected status: %d", resp.StatusCode) } // Cleanup err = mcpServer.Close() assert.NoError(t, err, "MCP server should close without error") } // TestMCPServerToolCall tests calling a specific tool (analyze) through the MCP server func TestMCPServerToolCall(t *testing.T) { logger, _ := zap.NewDevelopment() defer func() { err := logger.Sync() if err != nil { t.Logf("logger.Sync() error: %v", err) } }() aiProvider := &ai.AIProvider{ Name: "test-provider", Password: "test-password", Model: "test-model", } mcpServer, err := NewMCPServer("8090", aiProvider, true, logger) assert.NoError(t, err, "Should be able to create MCP server") // For HTTP mode, the server is already started in NewMCPServer // No need to call Start() as it's already running in a goroutine // Wait for the server to start err = waitForPort("localhost:8090", 10*time.Second) if err != nil { t.Skipf("MCP server did not start within timeout: %v", err) } // First, initialize the session initRequest := `{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-03-26", "capabilities": { "tools": {}, "resources": {}, "prompts": {} }, "clientInfo": { "name": "test-client", "version": "1.0.0" } } }` initResp, err := http.Post("http://localhost:8090/mcp", "application/json", bytes.NewBufferString(initRequest)) if err != nil { t.Logf("Initialize request failed: %v", err) return } defer func() { if err := initResp.Body.Close(); err != nil { t.Logf("Error closing init response body: %v", err) } }() // Extract session ID from response headers if present sessionID := initResp.Header.Get("Mcp-Session-Id") // Test calling the analyze tool with proper JSON-RPC format analyzeRequest := `{ "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "analyze", "arguments": { "namespace": "default", "backend": "openai", "language": "english", "explain": true, "maxConcurrency": 10 } } }` // Create request with session ID if available req, err := http.NewRequest("POST", "http://localhost:8090/mcp", bytes.NewBufferString(analyzeRequest)) if err != nil { t.Logf("Failed to create request: %v", err) return } req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json,text/event-stream") if sessionID != "" { req.Header.Set("Mcp-Session-Id", sessionID) } client := &http.Client{} resp, err := client.Do(req) if err != nil { t.Logf("Analyze tool call test skipped (server might not be fully ready): %v", err) return } defer func() { err := resp.Body.Close() if err != nil { t.Logf("resp.Body.Close() error: %v", err) } }() // Accept both 200 and 404 as valid responses if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNotFound { t.Errorf("Analyze tool call returned unexpected status: %d", resp.StatusCode) } // Cleanup err = mcpServer.Close() assert.NoError(t, err, "MCP server should close without error") } func waitForPort(address string, timeout time.Duration) error { start := time.Now() for { conn, err := net.Dial("tcp", address) if err == nil { _ = conn.Close() return nil } if time.Since(start) > timeout { return err } time.Sleep(100 * time.Millisecond) } } ================================================ FILE: pkg/util/util.go ================================================ /* Copyright 2023 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package util import ( "bytes" "context" "crypto/rand" "crypto/sha256" "encoding/base64" "encoding/hex" "errors" "fmt" "net/http" "os" "regexp" "strings" "k8s.io/apimachinery/pkg/labels" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" k "k8s.io/client-go/kubernetes" ) var anonymizePattern = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_=+[]{}|;':\",./<>?") func GetParent(client *kubernetes.Client, meta metav1.ObjectMeta) (string, bool) { if meta.OwnerReferences != nil { for _, owner := range meta.OwnerReferences { switch owner.Kind { case "ReplicaSet": rs, err := client.GetClient().AppsV1().ReplicaSets(meta.Namespace).Get(context.Background(), owner.Name, metav1.GetOptions{}) if err != nil { return "", false } if rs.OwnerReferences != nil { return GetParent(client, rs.ObjectMeta) } return "ReplicaSet/" + rs.Name, true case "Deployment": dep, err := client.GetClient().AppsV1().Deployments(meta.Namespace).Get(context.Background(), owner.Name, metav1.GetOptions{}) if err != nil { return "", false } if dep.OwnerReferences != nil { return GetParent(client, dep.ObjectMeta) } return "Deployment/" + dep.Name, true case "StatefulSet": sts, err := client.GetClient().AppsV1().StatefulSets(meta.Namespace).Get(context.Background(), owner.Name, metav1.GetOptions{}) if err != nil { return "", false } if sts.OwnerReferences != nil { return GetParent(client, sts.ObjectMeta) } return "StatefulSet/" + sts.Name, true case "DaemonSet": ds, err := client.GetClient().AppsV1().DaemonSets(meta.Namespace).Get(context.Background(), owner.Name, metav1.GetOptions{}) if err != nil { return "", false } if ds.OwnerReferences != nil { return GetParent(client, ds.ObjectMeta) } return "DaemonSet/" + ds.Name, true case "Ingress": ds, err := client.GetClient().NetworkingV1().Ingresses(meta.Namespace).Get(context.Background(), owner.Name, metav1.GetOptions{}) if err != nil { return "", false } if ds.OwnerReferences != nil { return GetParent(client, ds.ObjectMeta) } return "Ingress/" + ds.Name, true case "MutatingWebhookConfiguration": mw, err := client.GetClient().AdmissionregistrationV1().MutatingWebhookConfigurations().Get(context.Background(), owner.Name, metav1.GetOptions{}) if err != nil { return "", false } if mw.OwnerReferences != nil { return GetParent(client, mw.ObjectMeta) } return "MutatingWebhook/" + mw.Name, true case "ValidatingWebhookConfiguration": vw, err := client.GetClient().AdmissionregistrationV1().ValidatingWebhookConfigurations().Get(context.Background(), owner.Name, metav1.GetOptions{}) if err != nil { return "", false } if vw.OwnerReferences != nil { return GetParent(client, vw.ObjectMeta) } return "ValidatingWebhook/" + vw.Name, true } } } return "", false } func RemoveDuplicates(slice []string) ([]string, []string) { set := make(map[string]bool) duplicates := []string{} for _, val := range slice { if _, ok := set[val]; !ok { set[val] = true } else { duplicates = append(duplicates, val) } } uniqueSlice := make([]string, 0, len(set)) for val := range set { uniqueSlice = append(uniqueSlice, val) } return uniqueSlice, duplicates } func SliceDiff(source, dest []string) []string { mb := make(map[string]struct{}, len(dest)) for _, x := range dest { mb[x] = struct{}{} } var diff []string for _, x := range source { if _, found := mb[x]; !found { diff = append(diff, x) } } return diff } func MaskString(input string) string { key := make([]byte, len(input)) result := make([]rune, len(input)) _, err := rand.Read(key) if err != nil { panic(err) } for i := range result { result[i] = anonymizePattern[int(key[i])%len(anonymizePattern)] } return base64.StdEncoding.EncodeToString([]byte(string(result))) } func ReplaceIfMatch(text string, pattern string, replacement string) string { re := regexp.MustCompile(fmt.Sprintf(`%s(\b)`, pattern)) if re.MatchString(text) { text = re.ReplaceAllString(text, replacement) } return text } func GetCacheKey(provider string, language string, sEnc string) string { data := fmt.Sprintf("%s-%s-%s", provider, language, sEnc) hash := sha256.Sum256([]byte(data)) return hex.EncodeToString(hash[:]) } func GetPodListByLabels(client k.Interface, namespace string, labels map[string]string, ) (*v1.PodList, error) { pods, err := client.CoreV1().Pods(namespace).List(context.Background(), metav1.ListOptions{ LabelSelector: metav1.FormatLabelSelector(&metav1.LabelSelector{ MatchLabels: labels, }), }) if err != nil { return nil, err } return pods, nil } func FileExists(path string) (bool, error) { if _, err := os.Stat(path); err == nil { return true, nil } else if errors.Is(err, os.ErrNotExist) { return false, nil } else { return false, err } } func EnsureDirExists(dir string) error { err := os.MkdirAll(dir, 0o755) if errors.Is(err, os.ErrExist) { return nil } return err } func MapToString(m map[string]string) string { // Handle empty map case if len(m) == 0 { return "" } var pairs []string for k, v := range m { pairs = append(pairs, fmt.Sprintf("%s=%s", k, v)) } // Efficient string joining return strings.Join(pairs, ",") } func LabelsIncludeAny(predefinedSelector, Labels map[string]string) bool { // Check if any label in the predefinedSelector exists in Labels for key := range predefinedSelector { if _, exists := Labels[key]; exists { return true } } return false } func FetchLatestEvent(ctx context.Context, kubernetesClient *kubernetes.Client, namespace string, name string) (*v1.Event, error) { // get the list of events events, err := kubernetesClient.GetClient().CoreV1().Events(namespace).List(ctx, metav1.ListOptions{ FieldSelector: "involvedObject.name=" + name, }) if err != nil { return nil, err } // find most recent event var latestEvent *v1.Event for _, event := range events.Items { if latestEvent == nil { // this is required, as a pointer to a loop variable would always yield the latest value in the range e := event latestEvent = &e } if event.LastTimestamp.After(latestEvent.LastTimestamp.Time) { // this is required, as a pointer to a loop variable would always yield the latest value in the range e := event latestEvent = &e } } return latestEvent, nil } // NewHeaders parses a slice of strings in the format "key:value" into []http.Header // It handles headers with the same key by appending values func NewHeaders(customHeaders []string) []http.Header { headers := make(map[string][]string) for _, header := range customHeaders { vals := strings.SplitN(header, ":", 2) if len(vals) != 2 { //TODO: Handle error instead of ignoring it continue } key := strings.TrimSpace(vals[0]) value := strings.TrimSpace(vals[1]) if _, ok := headers[key]; !ok { headers[key] = []string{} } headers[key] = append(headers[key], value) } // Convert map to []http.Header format var result []http.Header for key, values := range headers { header := make(http.Header) for _, value := range values { header.Add(key, value) } result = append(result, header) } return result } func LabelStrToSelector(labelStr string) labels.Selector { if labelStr == "" { return nil } labelSelectorMap := make(map[string]string) for _, s := range strings.Split(labelStr, ",") { parts := strings.SplitN(s, "=", 2) if len(parts) == 2 { labelSelectorMap[parts[0]] = parts[1] } } return labels.SelectorFromSet(labels.Set(labelSelectorMap)) } // CaptureOutput captures the output of a function that writes to stdout func CaptureOutput(f func()) string { old := os.Stdout r, w, err := os.Pipe() if err != nil { panic(fmt.Sprintf("failed to create pipe: %v", err)) } os.Stdout = w // Ensure os.Stdout is restored even if panic occurs defer func() { os.Stdout = old }() f() if err := w.Close(); err != nil { panic(fmt.Sprintf("failed to close writer: %v", err)) } var buf bytes.Buffer if _, err := buf.ReadFrom(r); err != nil { panic(fmt.Sprintf("failed to read from pipe: %v", err)) } return buf.String() } // Contains checks if substr is present in s func Contains(s, substr string) bool { return bytes.Contains([]byte(s), []byte(substr)) } ================================================ FILE: pkg/util/util_test.go ================================================ /* Copyright 2024 The K8sGPT Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package util import ( "encoding/base64" "fmt" "testing" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" "github.com/stretchr/testify/require" admissionregistrationv1 "k8s.io/api/admissionregistration/v1" appsv1 "k8s.io/api/apps/v1" v1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/client-go/kubernetes/fake" ) func TestGetParent(t *testing.T) { ownerName := "test-name" namespace := "test" clientset := fake.NewSimpleClientset( &appsv1.ReplicaSet{ ObjectMeta: metav1.ObjectMeta{ Name: ownerName, Namespace: namespace, }, }, &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: ownerName, Namespace: namespace, }, }, &appsv1.StatefulSet{ ObjectMeta: metav1.ObjectMeta{ Name: ownerName, Namespace: namespace, }, }, &appsv1.DaemonSet{ ObjectMeta: metav1.ObjectMeta{ Name: ownerName, Namespace: namespace, }, }, &networkingv1.Ingress{ ObjectMeta: metav1.ObjectMeta{ Name: ownerName, Namespace: namespace, }, }, &admissionregistrationv1.MutatingWebhookConfiguration{ ObjectMeta: metav1.ObjectMeta{ Name: ownerName, }, }, &admissionregistrationv1.ValidatingWebhookConfiguration{ ObjectMeta: metav1.ObjectMeta{ Name: ownerName, }, }, ) kubeClient := kubernetes.Client{ Client: clientset, } tests := []struct { name string kind string expectedOutput string }{ { kind: "Unknown", expectedOutput: "", }, { kind: "ReplicaSet", }, { kind: "ReplicaSet", name: ownerName, expectedOutput: "ReplicaSet/test-name", }, { kind: "Deployment", }, { kind: "Deployment", name: ownerName, expectedOutput: "Deployment/test-name", }, { kind: "StatefulSet", }, { kind: "StatefulSet", name: ownerName, expectedOutput: "StatefulSet/test-name", }, { kind: "DaemonSet", }, { kind: "DaemonSet", name: ownerName, expectedOutput: "DaemonSet/test-name", }, { kind: "Ingress", }, { kind: "Ingress", name: ownerName, expectedOutput: "Ingress/test-name", }, { kind: "MutatingWebhookConfiguration", }, { kind: "MutatingWebhookConfiguration", name: ownerName, expectedOutput: "MutatingWebhook/test-name", }, { kind: "ValidatingWebhookConfiguration", }, { kind: "ValidatingWebhookConfiguration", name: ownerName, expectedOutput: "ValidatingWebhook/test-name", }, } for _, tt := range tests { tt := tt t.Run(tt.kind, func(t *testing.T) { meta := metav1.ObjectMeta{ Namespace: namespace, Name: ownerName, OwnerReferences: []metav1.OwnerReference{ { Kind: tt.kind, Name: tt.name, }, }, } output, ok := GetParent(&kubeClient, meta) if meta.OwnerReferences[0].Name != "" { require.Equal(t, true, ok) } else { require.Equal(t, false, ok) } require.Equal(t, tt.expectedOutput, output) }) } } func TestRemoveDuplicates(t *testing.T) { tests := []struct { name string slice []string expectedDuplicates []string }{ { name: "all empty", expectedDuplicates: []string{}, }, { name: "all unique", slice: []string{"temp", "value"}, expectedDuplicates: []string{}, }, { name: "slice not unique", slice: []string{"temp", "mango", "mango"}, expectedDuplicates: []string{"mango"}, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { _, duplicates := RemoveDuplicates(tt.slice) require.Equal(t, tt.expectedDuplicates, duplicates) }) } } func TestSliceDiff(t *testing.T) { tests := []struct { name string source []string dest []string expectedDiff []string }{ { name: "all empty", }, { name: "non empty", source: []string{"temp"}, dest: []string{"random"}, expectedDiff: []string{"temp"}, }, { name: "no diff", source: []string{"temp", "random"}, dest: []string{"random", "temp"}, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { require.Equal(t, tt.expectedDiff, SliceDiff(tt.source, tt.dest)) }) } } func TestReplaceIfMatch(t *testing.T) { tests := []struct { text string pattern string replacement string expectedOutput string }{ { text: "", expectedOutput: "", }, { text: "some value", pattern: "new", replacement: "latest", expectedOutput: "some value", }, { text: "new value", pattern: "value", replacement: "day", expectedOutput: "new day", }, } for _, tt := range tests { tt := tt t.Run(tt.text, func(t *testing.T) { require.Equal(t, tt.expectedOutput, ReplaceIfMatch(tt.text, tt.pattern, tt.replacement)) }) } } func TestGetCacheKey(t *testing.T) { tests := []struct { provider string language string sEnc string expectedOutput string }{ { expectedOutput: "d8156bae0c4243d3742fc4e9774d8aceabe0410249d720c855f98afc88ff846c", }, { provider: "provider", language: "english", sEnc: "encoding", expectedOutput: "39415cc324b1553b93e80e46049e4e4dbb752dc7d0424b2c6ac96d745c6392aa", }, } for _, tt := range tests { tt := tt t.Run(tt.language, func(t *testing.T) { require.Equal(t, tt.expectedOutput, GetCacheKey(tt.provider, tt.language, tt.sEnc)) }) } } func TestGetPodListByLabels(t *testing.T) { namespace1 := "test1" namespace2 := "test2" clientset := fake.NewSimpleClientset( &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "Pod1", Namespace: namespace1, Labels: map[string]string{ "Name": "Pod1", "Namespace": namespace1, }, }, }, &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "Pod2", Namespace: namespace2, Labels: map[string]string{ "Name": "Pod2", "Namespace": namespace2, }, }, }, &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "Pod3", Namespace: namespace1, Labels: map[string]string{ "Name": "Pod3", "Namespace": namespace1, }, }, }, &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "Pod4", Namespace: namespace2, Labels: map[string]string{ "Name": "Pod4", "Namespace": namespace2, }, }, }, ) tests := []struct { name string namespace string labels map[string]string expectedLen int expectedErr string }{ { name: "Name is Pod1", namespace: namespace1, labels: map[string]string{ "Name": "Pod1", }, expectedLen: 1, }, { name: "Name is Pod2 in namespace1", namespace: namespace1, labels: map[string]string{ "Name": "Pod2", }, expectedLen: 0, }, { name: "Name is Pod2 in namespace 2", namespace: namespace2, labels: map[string]string{ "Name": "Pod2", }, expectedLen: 1, }, { name: "All pods with namespace2 label in namespace1", namespace: namespace1, labels: map[string]string{ "Namespace": namespace2, }, expectedLen: 0, }, { name: "All pods with namespace2 label in namespace2", namespace: namespace2, labels: map[string]string{ "Namespace": namespace2, }, expectedLen: 2, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { pl, err := GetPodListByLabels(clientset, tt.namespace, tt.labels) if tt.expectedErr == "" { require.NoError(t, err) require.Equal(t, tt.expectedLen, len(pl.Items)) } else { require.ErrorContains(t, err, tt.expectedErr) require.Nil(t, pl) } }) } } func TestFileExists(t *testing.T) { tests := []struct { filePath string isPresent bool err string }{ { filePath: "", isPresent: false, }, { filePath: "./util.go", isPresent: true, }, } for _, tt := range tests { tt := tt t.Run(tt.filePath, func(t *testing.T) { isPresent, err := FileExists(tt.filePath) if tt.err == "" { require.NoError(t, err) } else { require.ErrorContains(t, err, tt.err) } require.Equal(t, tt.isPresent, isPresent) }) } } func TestEnsureDirExists(t *testing.T) { tests := []struct { dir string err string }{ { dir: "", err: "mkdir : no such file or directory", }, { dir: "./", }, } for _, tt := range tests { tt := tt t.Run(tt.dir, func(t *testing.T) { err := EnsureDirExists(tt.dir) if tt.err == "" { require.NoError(t, err) } else { require.ErrorContains(t, err, tt.err) } }) } } func TestMapToString(t *testing.T) { tests := []struct { name string m map[string]string output string }{ { name: "empty map", m: map[string]string{}, }, { name: "non-empty map", m: map[string]string{ "key": "value", }, output: "key=value", }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { require.Equal(t, tt.output, MapToString(tt.m)) }) } } func TestLabelsIncludeAny(t *testing.T) { tests := []struct { name string m map[string]string p map[string]string ok bool }{ { name: "empty map", m: map[string]string{}, p: map[string]string{}, ok: false, }, { name: "non-empty map", m: map[string]string{ "key": "value", }, p: map[string]string{ "key": "value", }, ok: true, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { require.Equal(t, tt.ok, LabelsIncludeAny(tt.p, tt.m)) }) } } func TestMaskString(t *testing.T) { input := "mysecret" masked := MaskString(input) // decode base64 to compare properties decoded, err := base64.StdEncoding.DecodeString(masked) require.NoError(t, err) require.Len(t, decoded, len(input)) // ensure it is not equal to input require.NotEqual(t, input, string(decoded)) // ensure all runes are from anonymizePattern allowed := make(map[rune]struct{}) for _, r := range anonymizePattern { allowed[r] = struct{}{} } for _, r := range string(decoded) { _, ok := allowed[r] require.True(t, ok, "unexpected rune: %q", r) } } func TestNewHeaders(t *testing.T) { input := []string{ "X-Test: foo", "X-Test: bar", "Content-Type: application/json", "InvalidHeader", // should be ignored } hs := NewHeaders(input) // flatten to a map for easier assertions got := map[string][]string{} for _, h := range hs { for k, v := range h { got[k] = append(got[k], v...) } } // expected values require.Contains(t, got, "X-Test") require.Contains(t, got, "Content-Type") // order of values is not guaranteed require.ElementsMatch(t, []string{"foo", "bar"}, got["X-Test"]) require.ElementsMatch(t, []string{"application/json"}, got["Content-Type"]) } func TestLabelStrToSelector(t *testing.T) { // empty case returns nil require.Nil(t, LabelStrToSelector("")) sel := LabelStrToSelector("key=value,foo=bar") require.NotNil(t, sel) // matches exact set m := map[string]string{"key": "value", "foo": "bar"} require.True(t, sel.Matches(labels.Set(m))) // does not match different values m2 := map[string]string{"key": "other", "foo": "bar"} require.False(t, sel.Matches(labels.Set(m2))) } func TestCaptureOutput(t *testing.T) { out := CaptureOutput(func() { fmt.Print("hello world") }) require.Equal(t, "hello world", out) } func TestContains(t *testing.T) { require.True(t, Contains("abcdef", "bcd")) require.False(t, Contains("abcdef", "xyz")) } ================================================ FILE: release-please-config.json ================================================ { "packages": { ".": { "changelog-path": "CHANGELOG.md", "release-type": "go", "prerelease": false, "bump-minor-pre-major": true, "bump-patch-for-minor-pre-major": true, "draft": false, "extra-files": [ "README.md", "deploy/manifest.yaml", "chart/Chart.yaml", "chart/values.yaml", "container/manifests/deployment.yaml" ], "changelog-sections": [ { "type": "feat", "section": "Features" }, { "type": "fix", "section": "Bug Fixes" }, { "type": "chore", "section": "Other" }, { "type": "docs", "section": "Docs" }, { "type": "perf", "section": "Performance" }, { "type": "build", "hidden": true, "section": "Build" }, { "type": "deps", "section": "Dependency Updates" }, { "type": "ci", "hidden": true, "section": "CI" }, { "type": "refactor", "section": "Refactoring" }, { "type": "revert", "hidden": true, "section": "Reverts" }, { "type": "style", "hidden": true, "section": "Styling" }, { "type": "test", "hidden": true, "section": "Tests" } ] } }, "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json" } ================================================ FILE: renovate.json ================================================ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ "config:base", "helpers:pinGitHubActionDigests", ":gitSignOff", "group:allNonMajor" ], "addLabels": ["dependencies"], "postUpdateOptions": [ "gomodTidy", "gomodMassage" ], "automerge": true, "automergeType": "pr", "schedule": [ "at any time" ], "platformAutomerge": true, "packageRules": [ { "matchPackageNames": ["azure-sdk-for-go"], "enabled": true, "groupName": "azure-group" }, { "matchPackageNames": ["prometheus"], "enabled": true, "groupName": "prometheus-group" }, { "matchPackageNames": ["k8s.io", "sigs.k8s.io"], "enabled": true, "groupName": "kubernetes-group" }, { "matchPackageNames": ["golang"], "enabled": true, "groupName": "golang-group" }, { "matchUpdateTypes": ["minor", "patch"], "matchCurrentVersion": "!/^0/", "automerge": true }, { "matchManagers": ["gomod"], "addLabels": ["go"] }, { "matchManagers": ["github-actions"], "addLabels": ["github_actions"] }, { "matchManagers": ["dockerfile"], "addLabels": ["docker"] } ], "regexManagers": [ { "fileMatch": [ "(^|\\/)Makefile$", "(^|\\/)Dockerfile", "(^|\\/).*\\.ya?ml$", "(^|\\/).*\\.toml$", "(^|\\/).*\\.sh$" ], "matchStrings": [ "# renovate: datasource=(?.+?) depName=(?.+?)\\s.*?_VERSION ?(\\??=|\\: ?) ?\\\"?(?.+?)?\\\"?\\s" ] } ] }