mirror of
https://github.com/k8sgpt-ai/k8sgpt.git
synced 2026-03-19 03:23:47 +00:00
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64f359c428 | ||
|
|
1acb22efdb | ||
|
|
8615ea28ed | ||
|
|
a7cff482a8 | ||
|
|
6e7c583aec | ||
|
|
9121a983e5 | ||
|
|
9642202ed1 | ||
|
|
0f88edf4e3 | ||
|
|
e5a8c57877 | ||
|
|
842f08c655 | ||
|
|
3988eb2fd0 | ||
|
|
f0a0c9aebf | ||
|
|
ec2e7703c6 | ||
|
|
a3becc9906 | ||
|
|
ffde363588 | ||
|
|
03a95e7b2a | ||
|
|
56a323c129 | ||
|
|
f20c139b1c | ||
|
|
960ba568d0 | ||
|
|
0071e25992 | ||
|
|
dee435514d | ||
|
|
fe2c08cf72 | ||
|
|
51b1b352ac | ||
|
|
bbf159455a | ||
|
|
e7076ed609 | ||
|
|
6247a1c0f3 | ||
|
|
a8f8070e16 | ||
|
|
92e7b3d3fb |
@@ -1 +1 @@
|
||||
{".":"0.2.2"}
|
||||
{".":"0.2.3"}
|
||||
28
CHANGELOG.md
28
CHANGELOG.md
@@ -1,5 +1,33 @@
|
||||
# Changelog
|
||||
|
||||
## [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)
|
||||
|
||||
|
||||
|
||||
16
Makefile
Normal file
16
Makefile
Normal file
@@ -0,0 +1,16 @@
|
||||
.PHONY: docker-build
|
||||
IMG ?= ghcr.io/k8sgpt-ai/k8sgpt:latest
|
||||
|
||||
deploy:
|
||||
ifndef SECRET
|
||||
$(error SECRET environment variable is not set)
|
||||
endif
|
||||
kubectl create ns k8sgpt || true
|
||||
kubectl create secret generic ai-backend-secret --from-literal=secret-key=$(SECRET) --namespace=k8sgpt || true
|
||||
kubectl apply -f container/manifests
|
||||
undeploy:
|
||||
kubectl delete secret ai-backend-secret --namespace=k8sgpt
|
||||
kubectl delete -f container/manifests
|
||||
kubectl delete ns k8sgpt
|
||||
docker-build:
|
||||
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
|
||||
41
README.md
41
README.md
@@ -28,16 +28,16 @@ brew install k8sgpt
|
||||
**32 bit:**
|
||||
<!---x-release-please-start-version-->
|
||||
```
|
||||
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.2.2/k8sgpt_386.rpm
|
||||
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.2.3/k8sgpt_386.rpm
|
||||
sudo rpm -ivh k8sgpt_386.rpm
|
||||
```
|
||||
<!---x-release-please-end-->
|
||||
|
||||
**64 bit:**
|
||||
|
||||
|
||||
<!---x-release-please-start-version-->
|
||||
```
|
||||
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.2.2/k8sgpt_amd64.rpm
|
||||
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.2.3/k8sgpt_amd64.rpm
|
||||
sudo rpm -ivh -i k8sgpt_amd64.rpm
|
||||
```
|
||||
<!---x-release-please-end-->
|
||||
@@ -49,15 +49,15 @@ brew install k8sgpt
|
||||
**32 bit:**
|
||||
<!---x-release-please-start-version-->
|
||||
```
|
||||
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.2.2/k8sgpt_386.deb
|
||||
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.2.3/k8sgpt_386.deb
|
||||
sudo dpkg -i k8sgpt_386.deb
|
||||
```
|
||||
<!---x-release-please-end-->
|
||||
**64 bit:**
|
||||
|
||||
|
||||
<!---x-release-please-start-version-->
|
||||
```
|
||||
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.2.2/k8sgpt_amd64.deb
|
||||
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.2.3/k8sgpt_amd64.deb
|
||||
sudo dpkg -i k8sgpt_amd64.deb
|
||||
```
|
||||
<!---x-release-please-end-->
|
||||
@@ -70,14 +70,14 @@ brew install k8sgpt
|
||||
**32 bit:**
|
||||
<!---x-release-please-start-version-->
|
||||
```
|
||||
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.2.2/k8sgpt_386.apk
|
||||
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.2.3/k8sgpt_386.apk
|
||||
apk add k8sgpt_386.apk
|
||||
```
|
||||
<!---x-release-please-end-->
|
||||
**64 bit:**
|
||||
<!---x-release-please-start-version-->
|
||||
```
|
||||
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.2.2/k8sgpt_amd64.apk
|
||||
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.2.3/k8sgpt_amd64.apk
|
||||
apk add k8sgpt_amd64.apk
|
||||
```
|
||||
<!---x-release-please-end-->x
|
||||
@@ -88,7 +88,7 @@ brew install k8sgpt
|
||||
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
|
||||
==> 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.
|
||||
```
|
||||
|
||||
@@ -102,7 +102,7 @@ If you install gcc as suggested, the problem will persist. Therefore, you need t
|
||||
|
||||
## Windows
|
||||
|
||||
* Download the latest Windows binaries of **k8sgpt** from the [Release](https://github.com/k8sgpt-ai/k8sgpt/releases)
|
||||
* 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* variable with the binary location
|
||||
|
||||
@@ -117,7 +117,7 @@ If you install gcc as suggested, the problem will persist. Therefore, you need t
|
||||
|
||||
* 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` to set it in k8sgpt.
|
||||
* Run `k8sgpt auth` 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.
|
||||
@@ -127,7 +127,7 @@ If you install gcc as suggested, the problem will persist. Therefore, you need t
|
||||
|
||||
## Analyzers
|
||||
|
||||
K8sGPT uses analyzers to triage and diagnose issues in your cluster. It has a set of analyzers that are built in, but
|
||||
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
|
||||
@@ -143,6 +143,7 @@ you will be able to write your own analyzers.
|
||||
- [x] statefulSetAnalyzer
|
||||
- [x] deploymentAnalyzer
|
||||
- [x] cronJobAnalyzer
|
||||
- [x] nodeAnalyzer
|
||||
|
||||
#### Optional
|
||||
|
||||
@@ -274,17 +275,25 @@ The Kubernetes system is trying to scale a StatefulSet named fake-deployment usi
|
||||
|
||||
## What about kubectl-ai?
|
||||
|
||||
The kubectl-ai [project](https://github.com/sozercan/kubectl-ai) uses AI to create manifests and apply them to the
|
||||
The kubectl-ai [project](https://github.com/sozercan/kubectl-ai) uses AI to create manifests and apply them to the
|
||||
cluster. It is not what we are trying to do here, it is focusing on writing YAML manifests.
|
||||
|
||||
K8sgpt is focused on triaging and diagnosing issues in your cluster. It is a tool for SRE, Platform & DevOps engineers
|
||||
to help them understand what is going on in their cluster. Cutting through the noise of logs and multiple tools to find
|
||||
K8sgpt is focused on triaging and diagnosing issues in your cluster. It is a tool for SRE, Platform & DevOps engineers
|
||||
to help them understand what is going on in their cluster. Cutting through the noise of logs and multiple tools to find
|
||||
the root cause of an issue.
|
||||
|
||||
|
||||
## Configuration
|
||||
|
||||
`k8sgpt` stores config data in `~/.k8sgpt.yaml` the data is stored in plain text, including your OpenAI key.
|
||||
`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 |
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
|
||||
@@ -51,17 +51,12 @@ var AnalyzeCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
// print results
|
||||
switch output {
|
||||
case "json":
|
||||
output, err := config.JsonOutput()
|
||||
if err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println(string(output))
|
||||
default:
|
||||
config.PrintOutput()
|
||||
output, err := config.PrintOutput(output)
|
||||
if err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println(string(output))
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
61
cmd/root.go
61
cmd/root.go
@@ -1,10 +1,13 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/k8sgpt-ai/k8sgpt/cmd/serve"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/adrg/xdg"
|
||||
"github.com/fatih/color"
|
||||
"github.com/k8sgpt-ai/k8sgpt/cmd/serve"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
|
||||
"github.com/k8sgpt-ai/k8sgpt/cmd/analyze"
|
||||
"github.com/k8sgpt-ai/k8sgpt/cmd/auth"
|
||||
"github.com/k8sgpt-ai/k8sgpt/cmd/filters"
|
||||
@@ -43,6 +46,8 @@ func Execute(v string) {
|
||||
}
|
||||
|
||||
func init() {
|
||||
performConfigMigrationIfNeeded()
|
||||
|
||||
cobra.OnInitialize(initConfig)
|
||||
|
||||
var kubeconfigPath string
|
||||
@@ -66,14 +71,12 @@ func initConfig() {
|
||||
// Use config file from the flag.
|
||||
viper.SetConfigFile(cfgFile)
|
||||
} else {
|
||||
// Find home directory.
|
||||
home, err := os.UserHomeDir()
|
||||
cobra.CheckErr(err)
|
||||
// the config will belocated under `~/.config/k8sgpt/k8sgpt.yaml` on linux
|
||||
configDir := filepath.Join(xdg.ConfigHome, "k8sgpt")
|
||||
|
||||
// Search config in home directory with name ".k8sgpt.git" (without extension).
|
||||
viper.AddConfigPath(home)
|
||||
viper.AddConfigPath(configDir)
|
||||
viper.SetConfigType("yaml")
|
||||
viper.SetConfigName(".k8sgpt")
|
||||
viper.SetConfigName("k8sgpt")
|
||||
|
||||
viper.SafeWriteConfig()
|
||||
}
|
||||
@@ -81,6 +84,7 @@ func initConfig() {
|
||||
viper.Set("kubecontext", kubecontext)
|
||||
viper.Set("kubeconfig", kubeconfig)
|
||||
|
||||
viper.SetEnvPrefix("K8SGPT")
|
||||
viper.AutomaticEnv() // read in environment variables that match
|
||||
|
||||
// If a config file is found, read it in.
|
||||
@@ -88,3 +92,44 @@ func initConfig() {
|
||||
// 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 {
|
||||
fmt.Fprintln(os.Stderr, color.RedString("Warning: Legacy config file at `%s` detected! This file will be ignored!", oldConfig))
|
||||
return
|
||||
}
|
||||
|
||||
if oldConfigExists && !newConfigExists {
|
||||
fmt.Fprintln(os.Stderr, color.RedString("Performing config file migration from `%s` to `%s`", oldConfig, newConfig))
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package serve
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/ai"
|
||||
k8sgptserver "github.com/k8sgpt-ai/k8sgpt/pkg/server"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"os"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -21,30 +22,60 @@ var ServeCmd = &cobra.Command{
|
||||
Long: `Runs k8sgpt as a server to allow for easy integration with other applications.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
|
||||
backendType := viper.GetString("backend_type")
|
||||
if backendType == "" {
|
||||
color.Red("No backend set. Please run k8sgpt auth")
|
||||
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 {
|
||||
// Check for env injection
|
||||
backend = os.Getenv("K8SGPT_BACKEND")
|
||||
password := os.Getenv("K8SGPT_PASSWORD")
|
||||
model := os.Getenv("K8SGPT_MODEL")
|
||||
// If the envs are set, alocate in place to the aiProvider
|
||||
// else exit with error
|
||||
if backend != "" || password != "" || model != "" {
|
||||
aiProvider = &ai.AIProvider{
|
||||
Name: backend,
|
||||
Password: password,
|
||||
Model: model,
|
||||
}
|
||||
|
||||
if backend != "" {
|
||||
backendType = backend
|
||||
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 {
|
||||
aiProvider = &provider
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
token := viper.GetString(fmt.Sprintf("%s_key", backendType))
|
||||
// check if nil
|
||||
if token == "" {
|
||||
color.Red("No %s key set. Please run k8sgpt auth", backendType)
|
||||
if aiProvider.Name == "" {
|
||||
color.Red("Error: AI provider %s not specified in configuration. Please run k8sgpt auth", backend)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
server := k8sgptserver.Config{
|
||||
Backend: backend,
|
||||
Backend: aiProvider.Name,
|
||||
Port: port,
|
||||
Token: token,
|
||||
Token: aiProvider.Password,
|
||||
}
|
||||
|
||||
err := server.Serve()
|
||||
err = server.Serve()
|
||||
if err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
os.Exit(1)
|
||||
|
||||
@@ -10,7 +10,7 @@ var versionCmd = &cobra.Command{
|
||||
Short: "Print the version number of k8sgpt",
|
||||
Long: `All software has versions. This is k8sgpt's`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmd.Printf("k8sgpt version %s", version)
|
||||
cmd.Printf("k8sgpt version %s\n", version)
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
42
container/manifests/deployment.yaml
Normal file
42
container/manifests/deployment.yaml
Normal file
@@ -0,0 +1,42 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: k8sgpt-deployment
|
||||
namespace: k8sgpt
|
||||
labels:
|
||||
app: k8sgpt
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: k8sgpt
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: k8sgpt
|
||||
spec:
|
||||
serviceAccountName: k8sgpt
|
||||
containers:
|
||||
- name: k8sgpt-container
|
||||
imagePullPolicy: Always
|
||||
image: ghcr.io/k8sgpt-ai/k8sgpt:v0.2.3 #x-release-please-version
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
args: ["serve"]
|
||||
resources:
|
||||
limits:
|
||||
cpu: "1"
|
||||
memory: "512Mi"
|
||||
requests:
|
||||
cpu: "0.5"
|
||||
memory: "256Mi"
|
||||
env:
|
||||
- name: K8SGPT_MODEL
|
||||
value: "gpt-3.5-turbo"
|
||||
- name: K8SGPT_BACKEND
|
||||
value: "openai"
|
||||
- name: K8SGPT_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: ai-backend-secret
|
||||
key: secret-key
|
||||
13
container/manifests/role.yaml
Normal file
13
container/manifests/role.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: k8sgpt-cluster-role-all
|
||||
rules:
|
||||
- apiGroups:
|
||||
- '*'
|
||||
resources:
|
||||
- '*'
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
13
container/manifests/rolebinding.yaml
Normal file
13
container/manifests/rolebinding.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: k8sgpt-rolebinding
|
||||
namespace: k8sgpt
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: k8sgpt
|
||||
namespace: k8sgpt
|
||||
roleRef:
|
||||
kind: ClusterRole
|
||||
name: k8sgpt-cluster-role-all
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
5
container/manifests/sa.yaml
Normal file
5
container/manifests/sa.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: k8sgpt
|
||||
namespace: k8sgpt
|
||||
13
container/manifests/service.yaml
Normal file
13
container/manifests/service.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: k8sgpt-service
|
||||
namespace: k8sgpt
|
||||
spec:
|
||||
selector:
|
||||
app: k8sgpt
|
||||
ports:
|
||||
- name: http
|
||||
port: 8080
|
||||
targetPort: 8080
|
||||
type: ClusterIP
|
||||
11
demo
Executable file
11
demo
Executable file
@@ -0,0 +1,11 @@
|
||||
. demo-magic.sh
|
||||
clear
|
||||
|
||||
pe "k8sgpt filter list"
|
||||
pe "k8sgpt analyze --filter=Pod --explain -o json | jq ."
|
||||
pe "k8sgpt integration list"
|
||||
pe "k8sgpt integration activate trivy"
|
||||
pe "k8sgpt filter list"
|
||||
pe "k8sgpt analyze --filter=VulnerabilityReport"
|
||||
pe "./k8sgpt analyze --filter=Node --explain"
|
||||
|
||||
4
go.mod
4
go.mod
@@ -7,7 +7,7 @@ require (
|
||||
github.com/fatih/color v1.15.0
|
||||
github.com/magiconair/properties v1.8.7
|
||||
github.com/mittwald/go-helm-client v0.12.1
|
||||
github.com/sashabaranov/go-openai v1.7.0
|
||||
github.com/sashabaranov/go-openai v1.8.0
|
||||
github.com/schollz/progressbar/v3 v3.13.1
|
||||
github.com/spf13/cobra v1.7.0
|
||||
github.com/spf13/viper v1.15.0
|
||||
@@ -21,6 +21,8 @@ require (
|
||||
|
||||
)
|
||||
|
||||
require github.com/adrg/xdg v0.4.0 // indirect
|
||||
|
||||
require (
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230106234847-43070de90fa1 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||
|
||||
7
go.sum
7
go.sum
@@ -69,6 +69,8 @@ github.com/ProtonMail/go-crypto v0.0.0-20221026131551-cf6655e29de4 h1:ra2OtmuW0A
|
||||
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs=
|
||||
github.com/a8m/expect v1.0.0/go.mod h1:4IwSCMumY49ScypDnjNbYEjgVeqy1/U2cEs3Lat96eA=
|
||||
github.com/acomagu/bufpipe v1.0.3 h1:fxAGrHZTgQ9w5QqVItgzwj235/uYZYgbXitB+dLupOk=
|
||||
github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls=
|
||||
github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E=
|
||||
github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
|
||||
github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
@@ -623,8 +625,8 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/samber/lo v1.37.0 h1:XjVcB8g6tgUp8rsPsJ2CvhClfImrpL04YpQHXeHPhRw=
|
||||
github.com/samber/lo v1.37.0/go.mod h1:9vaz2O4o8oOnK23pd2TrXufcbdbJIa3b6cstBWKpopA=
|
||||
github.com/sashabaranov/go-openai v1.7.0 h1:D1dBXoZhtf/aKNu6WFf0c7Ah2NM30PZ/3Mqly6cZ7fk=
|
||||
github.com/sashabaranov/go-openai v1.7.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
|
||||
github.com/sashabaranov/go-openai v1.8.0 h1:IZrNK/gGqxtp0j19F4NLGbmfoOkyDpM3oC9i/tv9bBM=
|
||||
github.com/sashabaranov/go-openai v1.8.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
|
||||
github.com/schollz/progressbar/v3 v3.13.1 h1:o8rySDYiQ59Mwzy2FELeHY5ZARXZTVJC7iHD6PEFUiE=
|
||||
github.com/schollz/progressbar/v3 v3.13.1/go.mod h1:xvrbki8kfT1fzWzBT/UZd9L6GA+jdL7HAgq2RFnO6fQ=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
@@ -946,6 +948,7 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
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-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/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-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
|
||||
BIN
images/demo5.gif
Normal file
BIN
images/demo5.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
BIN
images/nodes.gif
Normal file
BIN
images/nodes.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 120 KiB |
@@ -2,7 +2,6 @@ package analysis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
@@ -149,45 +148,6 @@ func (a *Analysis) RunAnalysis() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
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{
|
||||
Problems: problems,
|
||||
Results: a.Results,
|
||||
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) PrintOutput() {
|
||||
fmt.Println("")
|
||||
if len(a.Results) == 0 {
|
||||
fmt.Println(color.GreenString("No problems detected"))
|
||||
}
|
||||
for n, result := range a.Results {
|
||||
fmt.Printf("%s %s(%s)\n", color.CyanString("%d", n),
|
||||
color.YellowString(result.Name), color.CyanString(result.ParentObject))
|
||||
for _, err := range result.Error {
|
||||
fmt.Printf("- %s %s\n", color.RedString("Error:"), color.RedString(err.Text))
|
||||
}
|
||||
fmt.Println(color.GreenString(result.Details + "\n"))
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Analysis) GetAIResults(output string, anonymize bool) error {
|
||||
if len(a.Results) == 0 {
|
||||
return nil
|
||||
|
||||
@@ -22,7 +22,7 @@ func TestAnalysis_NoProblemJsonOutput(t *testing.T) {
|
||||
Results: []common.Result{},
|
||||
}
|
||||
|
||||
gotJson, err := analysis.JsonOutput()
|
||||
gotJson, err := analysis.PrintOutput("json")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -75,7 +75,7 @@ func TestAnalysis_ProblemJsonOutput(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
gotJson, err := analysis.JsonOutput()
|
||||
gotJson, err := analysis.PrintOutput("json")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -136,7 +136,7 @@ func TestAnalysis_MultipleProblemJsonOutput(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
gotJson, err := analysis.JsonOutput()
|
||||
gotJson, err := analysis.PrintOutput("json")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
72
pkg/analysis/output.go
Normal file
72
pkg/analysis/output.go
Normal file
@@ -0,0 +1,72 @@
|
||||
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{
|
||||
Problems: problems,
|
||||
Results: a.Results,
|
||||
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) textOutput() ([]byte, error) {
|
||||
var output strings.Builder
|
||||
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)\n", color.CyanString("%d", n),
|
||||
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)))
|
||||
}
|
||||
output.WriteString(color.GreenString(result.Details + "\n"))
|
||||
}
|
||||
return []byte(output.String()), nil
|
||||
}
|
||||
@@ -7,6 +7,15 @@ import (
|
||||
"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{
|
||||
@@ -18,6 +27,7 @@ var coreAnalyzerMap = map[string]common.IAnalyzer{
|
||||
"Ingress": IngressAnalyzer{},
|
||||
"StatefulSet": StatefulSetAnalyzer{},
|
||||
"CronJob": CronJobAnalyzer{},
|
||||
"Node": NodeAnalyzer{},
|
||||
}
|
||||
|
||||
var additionalAnalyzerMap = map[string]common.IAnalyzer{
|
||||
|
||||
@@ -13,6 +13,13 @@ import (
|
||||
type CronJobAnalyzer struct{}
|
||||
|
||||
func (analyzer CronJobAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
|
||||
kind := "CronJob"
|
||||
|
||||
AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{
|
||||
"analyzer_name": kind,
|
||||
})
|
||||
|
||||
var results []common.Result
|
||||
|
||||
cronJobList, err := a.Client.GetClient().BatchV1().CronJobs("").List(a.Context, v1.ListOptions{})
|
||||
@@ -81,14 +88,16 @@ func (analyzer CronJobAnalyzer) Analyze(a common.Analyzer) ([]common.Result, err
|
||||
}
|
||||
|
||||
if len(failures) > 0 {
|
||||
preAnalysis[cronJob.Name] = common.PreAnalysis{
|
||||
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: "CronJob",
|
||||
Kind: kind,
|
||||
Name: key,
|
||||
Error: value.FailureDetails,
|
||||
}
|
||||
|
||||
@@ -121,6 +121,6 @@ func TestCronJobBroken(t *testing.T) {
|
||||
}
|
||||
|
||||
assert.Equal(t, len(analysisResults), 1)
|
||||
assert.Equal(t, analysisResults[0].Name, "example-cronjob")
|
||||
assert.Equal(t, analysisResults[0].Name, "default/example-cronjob")
|
||||
assert.Equal(t, analysisResults[0].Kind, "CronJob")
|
||||
}
|
||||
|
||||
@@ -17,6 +17,12 @@ type DeploymentAnalyzer struct {
|
||||
// Analyze scans all namespaces for Deployments with misconfigurations
|
||||
func (d DeploymentAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
|
||||
kind := "Deployment"
|
||||
|
||||
AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{
|
||||
"analyzer_name": kind,
|
||||
})
|
||||
|
||||
deployments, err := a.Client.GetClient().AppsV1().Deployments("").List(context.Background(), v1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -44,13 +50,14 @@ func (d DeploymentAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error)
|
||||
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: "Deployment",
|
||||
Kind: kind,
|
||||
Name: key,
|
||||
Error: value.FailureDetails,
|
||||
}
|
||||
|
||||
@@ -12,6 +12,12 @@ type HpaAnalyzer struct{}
|
||||
|
||||
func (HpaAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
|
||||
kind := "HorizontalPodAutoscaler"
|
||||
|
||||
AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{
|
||||
"analyzer_name": kind,
|
||||
})
|
||||
|
||||
list, err := a.Client.GetClient().AutoscalingV1().HorizontalPodAutoscalers(a.Namespace).List(a.Context, metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -71,13 +77,14 @@ func (HpaAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
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: "HorizontalPodAutoscaler",
|
||||
Kind: kind,
|
||||
Name: key,
|
||||
Error: value.FailureDetails,
|
||||
}
|
||||
|
||||
@@ -12,6 +12,12 @@ type IngressAnalyzer struct{}
|
||||
|
||||
func (IngressAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
|
||||
kind := "Ingress"
|
||||
|
||||
AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{
|
||||
"analyzer_name": kind,
|
||||
})
|
||||
|
||||
list, err := a.Client.GetClient().NetworkingV1().Ingresses(a.Namespace).List(a.Context, metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -107,13 +113,15 @@ func (IngressAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
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: "Ingress",
|
||||
Kind: kind,
|
||||
Name: key,
|
||||
Error: value.FailureDetails,
|
||||
}
|
||||
|
||||
@@ -11,6 +11,13 @@ import (
|
||||
type NetworkPolicyAnalyzer struct{}
|
||||
|
||||
func (NetworkPolicyAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
|
||||
kind := "NetworkPolicy"
|
||||
|
||||
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{})
|
||||
@@ -54,16 +61,18 @@ func (NetworkPolicyAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error)
|
||||
}
|
||||
|
||||
if len(failures) > 0 {
|
||||
preAnalysis[policy.Name] = common.PreAnalysis{
|
||||
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: "NetworkPolicy",
|
||||
Kind: kind,
|
||||
Name: key,
|
||||
Error: value.FailureDetails,
|
||||
}
|
||||
|
||||
83
pkg/analyzer/node.go
Normal file
83
pkg/analyzer/node.go
Normal file
@@ -0,0 +1,83 @@
|
||||
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{})
|
||||
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 {
|
||||
break
|
||||
}
|
||||
failures = addNodeConditionFailure(failures, node.Name, nodeCondition)
|
||||
default:
|
||||
if nodeCondition.Status != v1.ConditionFalse {
|
||||
failures = addNodeConditionFailure(failures, node.Name, nodeCondition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(failures) > 0 {
|
||||
preAnalysis[fmt.Sprintf("%s", 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, _ := util.GetParent(a.Client, value.Node.ObjectMeta)
|
||||
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
|
||||
}
|
||||
111
pkg/analyzer/node_test.go
Normal file
111
pkg/analyzer/node_test.go
Normal file
@@ -0,0 +1,111 @@
|
||||
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"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
)
|
||||
|
||||
func TestNodeAnalyzerNodeReady(t *testing.T) {
|
||||
clientset := fake.NewSimpleClientset(&v1.Node{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "node1",
|
||||
},
|
||||
Status: v1.NodeStatus{
|
||||
Conditions: []v1.NodeCondition{
|
||||
{
|
||||
Type: v1.NodeReady,
|
||||
Status: v1.ConditionTrue,
|
||||
Reason: "KubeletReady",
|
||||
Message: "kubelet is posting ready status",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: clientset,
|
||||
},
|
||||
Context: context.Background(),
|
||||
}
|
||||
nodeAnalyzer := NodeAnalyzer{}
|
||||
var analysisResults []common.Result
|
||||
analysisResults, err := nodeAnalyzer.Analyze(config)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
assert.Equal(t, len(analysisResults), 0)
|
||||
}
|
||||
|
||||
func TestNodeAnalyzerNodeDiskPressure(t *testing.T) {
|
||||
clientset := fake.NewSimpleClientset(&v1.Node{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "node1",
|
||||
},
|
||||
Status: v1.NodeStatus{
|
||||
Conditions: []v1.NodeCondition{
|
||||
{
|
||||
Type: v1.NodeDiskPressure,
|
||||
Status: v1.ConditionTrue,
|
||||
Reason: "KubeletHasDiskPressure",
|
||||
Message: "kubelet has disk pressure",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: clientset,
|
||||
},
|
||||
Context: context.Background(),
|
||||
}
|
||||
nodeAnalyzer := NodeAnalyzer{}
|
||||
var analysisResults []common.Result
|
||||
analysisResults, err := nodeAnalyzer.Analyze(config)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
assert.Equal(t, len(analysisResults), 1)
|
||||
}
|
||||
|
||||
// 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
|
||||
func TestNodeAnalyzerNodeUnknownType(t *testing.T) {
|
||||
clientset := fake.NewSimpleClientset(&v1.Node{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "node1",
|
||||
},
|
||||
Status: v1.NodeStatus{
|
||||
Conditions: []v1.NodeCondition{
|
||||
{
|
||||
Type: "UnknownNodeConditionType",
|
||||
Status: "CompletelyUnknown",
|
||||
Reason: "KubeletHasTheUnknown",
|
||||
Message: "kubelet has the unknown",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: clientset,
|
||||
},
|
||||
Context: context.Background(),
|
||||
}
|
||||
nodeAnalyzer := NodeAnalyzer{}
|
||||
var analysisResults []common.Result
|
||||
analysisResults, err := nodeAnalyzer.Analyze(config)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
assert.Equal(t, len(analysisResults), 1)
|
||||
}
|
||||
@@ -12,6 +12,12 @@ type PdbAnalyzer struct{}
|
||||
|
||||
func (PdbAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
|
||||
kind := "PodDisruptionBudget"
|
||||
|
||||
AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{
|
||||
"analyzer_name": kind,
|
||||
})
|
||||
|
||||
list, err := a.Client.GetClient().PolicyV1().PodDisruptionBudgets(a.Namespace).List(a.Context, metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -63,12 +69,13 @@ func (PdbAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
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: "PodDisruptionBudget",
|
||||
Kind: kind,
|
||||
Name: key,
|
||||
Error: value.FailureDetails,
|
||||
}
|
||||
|
||||
@@ -12,6 +12,13 @@ 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{})
|
||||
if err != nil {
|
||||
@@ -70,12 +77,13 @@ func (PodAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
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: "Pod",
|
||||
Kind: kind,
|
||||
Name: key,
|
||||
Error: value.FailureDetails,
|
||||
}
|
||||
|
||||
@@ -12,6 +12,12 @@ 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{})
|
||||
if err != nil {
|
||||
@@ -43,12 +49,13 @@ func (PvcAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
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: "PersistentVolumeClaim",
|
||||
Kind: kind,
|
||||
Name: key,
|
||||
Error: value.FailureDetails,
|
||||
}
|
||||
|
||||
@@ -12,6 +12,12 @@ 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{})
|
||||
if err != nil {
|
||||
@@ -42,12 +48,13 @@ func (ReplicaSetAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
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: "ReplicaSet",
|
||||
Kind: kind,
|
||||
Name: key,
|
||||
Error: value.FailureDetails,
|
||||
}
|
||||
|
||||
@@ -13,6 +13,12 @@ type ServiceAnalyzer struct{}
|
||||
|
||||
func (ServiceAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
|
||||
kind := "Service"
|
||||
|
||||
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{})
|
||||
if err != nil {
|
||||
@@ -71,12 +77,13 @@ func (ServiceAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
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: "Service",
|
||||
Kind: kind,
|
||||
Name: key,
|
||||
Error: value.FailureDetails,
|
||||
}
|
||||
|
||||
@@ -11,6 +11,13 @@ import (
|
||||
type StatefulSetAnalyzer struct{}
|
||||
|
||||
func (StatefulSetAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
|
||||
kind := "StatefulSet"
|
||||
|
||||
AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{
|
||||
"analyzer_name": kind,
|
||||
})
|
||||
|
||||
list, err := a.Client.GetClient().AppsV1().StatefulSets(a.Namespace).List(a.Context, metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -61,12 +68,13 @@ func (StatefulSetAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
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: "StatefulSet",
|
||||
Kind: kind,
|
||||
Name: key,
|
||||
Error: value.FailureDetails,
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ type PreAnalysis struct {
|
||||
PodDisruptionBudget policyv1.PodDisruptionBudget
|
||||
StatefulSet appsv1.StatefulSet
|
||||
NetworkPolicy networkv1.NetworkPolicy
|
||||
Node v1.Node
|
||||
// Integrations
|
||||
TrivyVulnerabilityReport trivy.VulnerabilityReport
|
||||
}
|
||||
|
||||
@@ -3,10 +3,12 @@ package integration
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/integration/trivy"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
@@ -56,16 +58,24 @@ func (*Integration) Activate(name string, namespace string) error {
|
||||
return errors.New("integration not found")
|
||||
}
|
||||
|
||||
if err := integrations[name].Deploy(namespace); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Update filters
|
||||
activeFilters := viper.GetStringSlice("active_filters")
|
||||
|
||||
activeFilters = append(activeFilters, integrations[name].GetAnalyzerName())
|
||||
mergedFilters := append(activeFilters, integrations[name].GetAnalyzerName())
|
||||
|
||||
viper.Set("active_filters", activeFilters)
|
||||
uniqueFilters, dupplicatedFilters := util.RemoveDuplicates(mergedFilters)
|
||||
|
||||
// Verify dupplicate
|
||||
if len(dupplicatedFilters) != 0 {
|
||||
color.Red("Integration already activated : %s", strings.Join(dupplicatedFilters, ", "))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
viper.Set("active_filters", uniqueFilters)
|
||||
|
||||
if err := integrations[name].Deploy(namespace); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := viper.WriteConfig(); err != nil {
|
||||
color.Red("Error writing config file: %s", err.Error())
|
||||
@@ -80,21 +90,27 @@ func (*Integration) Deactivate(name string, namespace string) error {
|
||||
return errors.New("integration not found")
|
||||
}
|
||||
|
||||
if err := integrations[name].UnDeploy(namespace); err != nil {
|
||||
return err
|
||||
}
|
||||
activeFilters := viper.GetStringSlice("active_filters")
|
||||
|
||||
// Update filters
|
||||
// This might be a bad idea, but we cannot reference analyzer here
|
||||
activeFilters := viper.GetStringSlice("active_filters")
|
||||
|
||||
// Remove filter
|
||||
foundFilter := false
|
||||
for i, v := range activeFilters {
|
||||
if v == integrations[name].GetAnalyzerName() {
|
||||
foundFilter = true
|
||||
activeFilters = append(activeFilters[:i], activeFilters[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundFilter {
|
||||
color.Red("Ingregation %s does not exist in configuration file. Please use k8sgpt integration add.", name)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := integrations[name].UnDeploy(namespace); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
viper.Set("active_filters", activeFilters)
|
||||
|
||||
if err := viper.WriteConfig(); err != nil {
|
||||
|
||||
@@ -28,26 +28,29 @@ func (c *Client) GetRestClient() rest.Interface {
|
||||
}
|
||||
|
||||
func NewClient(kubecontext string, kubeconfig string) (*Client, error) {
|
||||
|
||||
config := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
|
||||
&clientcmd.ClientConfigLoadingRules{ExplicitPath: kubeconfig},
|
||||
&clientcmd.ConfigOverrides{
|
||||
CurrentContext: kubecontext,
|
||||
})
|
||||
// create the clientset
|
||||
c, err := config.ClientConfig()
|
||||
var config *rest.Config
|
||||
config, err := rest.InClusterConfig()
|
||||
if err != nil {
|
||||
clientConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
|
||||
&clientcmd.ClientConfigLoadingRules{ExplicitPath: kubeconfig},
|
||||
&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
|
||||
}
|
||||
clientSet, err := kubernetes.NewForConfig(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.APIPath = "/api"
|
||||
c.GroupVersion = &scheme.Scheme.PrioritizedVersionsForGroup("")[0]
|
||||
c.NegotiatedSerializer = serializer.WithoutConversionCodecFactory{CodecFactory: scheme.Codecs}
|
||||
config.APIPath = "/api"
|
||||
config.GroupVersion = &scheme.Scheme.PrioritizedVersionsForGroup("")[0]
|
||||
config.NegotiatedSerializer = serializer.WithoutConversionCodecFactory{CodecFactory: scheme.Codecs}
|
||||
|
||||
restClient, err := rest.RESTClientFor(c)
|
||||
restClient, err := rest.RESTClientFor(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -55,6 +58,6 @@ func NewClient(kubecontext string, kubeconfig string) (*Client, error) {
|
||||
return &Client{
|
||||
Client: clientSet,
|
||||
RestClient: restClient,
|
||||
Config: c,
|
||||
Config: config,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -3,11 +3,13 @@ package server
|
||||
import (
|
||||
json "encoding/json"
|
||||
"fmt"
|
||||
"github.com/fatih/color"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/analysis"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/analysis"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
@@ -40,6 +42,11 @@ func (s *Config) analyzeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
anonymize := getBoolParam(r.URL.Query().Get("anonymize"))
|
||||
nocache := getBoolParam(r.URL.Query().Get("nocache"))
|
||||
language := r.URL.Query().Get("language")
|
||||
s.Output = r.URL.Query().Get("output")
|
||||
|
||||
if s.Output == "" {
|
||||
s.Output = "json"
|
||||
}
|
||||
|
||||
config, err := analysis.NewAnalysis(s.Backend, language, []string{}, namespace, nocache, explain)
|
||||
if err != nil {
|
||||
@@ -63,17 +70,19 @@ func (s *Config) analyzeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
output, err := config.JsonOutput()
|
||||
out, err := config.PrintOutput(s.Output)
|
||||
if err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
health.Failure++
|
||||
fmt.Fprintf(w, err.Error())
|
||||
}
|
||||
|
||||
health.Success++
|
||||
fmt.Fprintf(w, string(output))
|
||||
fmt.Fprintf(w, string(out))
|
||||
}
|
||||
|
||||
func (s *Config) Serve() error {
|
||||
http.Handle("/metrics", promhttp.Handler())
|
||||
http.HandleFunc("/analyze", s.analyzeHandler)
|
||||
http.HandleFunc("/healthz", s.healthzHandler)
|
||||
color.Green("Starting server on port %s", s.Port)
|
||||
@@ -3,8 +3,10 @@ package util
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
"regexp"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
|
||||
@@ -150,3 +152,23 @@ func GetPodListByLabels(client k.Interface,
|
||||
|
||||
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.Mkdir(dir, 0755)
|
||||
|
||||
if errors.Is(err, os.ErrExist) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
"README.md",
|
||||
"deploy/manifest.yaml",
|
||||
"chart/Chart.yaml",
|
||||
"chart/values.yaml"
|
||||
"chart/values.yaml",
|
||||
"container/manifests/deployment.yaml"
|
||||
],
|
||||
"changelog-sections": [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user