mirror of
https://github.com/replicatedhq/troubleshoot.git
synced 2026-09-03 00:47:17 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4623d9404 | ||
|
|
bc4856869e | ||
|
|
f6373f3e36 | ||
|
|
7038da85b1 | ||
|
|
a7bb9ea31e | ||
|
|
08c3fcf3df | ||
|
|
1d2c1191e2 | ||
|
|
66e070a07d | ||
|
|
7528c57e83 | ||
|
|
42fc4f06de | ||
|
|
312e467160 | ||
|
|
5e280d0652 | ||
|
|
15cfdb9eee | ||
|
|
15a4802cd2 | ||
|
|
0d4d305ae6 | ||
|
|
de7112fed2 | ||
|
|
73a2d882d7 | ||
|
|
461cc994ef | ||
|
|
1bdf87e8cd | ||
|
|
32b0e1a890 | ||
|
|
cc56522571 | ||
|
|
e3adc1cb35 | ||
|
|
359475ba87 | ||
|
|
f3d49c657f | ||
|
|
7f3808b8d7 | ||
|
|
f44d077756 | ||
|
|
e34742f3cc | ||
|
|
fd29012624 | ||
|
|
be5e66cbca | ||
|
|
c079d96efe | ||
|
|
7b797b3725 | ||
|
|
6cbe188abe | ||
|
|
86279b4ec4 | ||
|
|
514c86d891 | ||
|
|
b9f4fc4390 | ||
|
|
53813502de | ||
|
|
597fb2996b | ||
|
|
e44aff4100 | ||
|
|
d0d6f00c0f | ||
|
|
4460e8c639 | ||
|
|
c166d46af4 | ||
|
|
ded1f42b5a | ||
|
|
32176178a8 | ||
|
|
949b645336 | ||
|
|
1f50fe3163 | ||
|
|
24c6278e5d | ||
|
|
8d527ead8c | ||
|
|
1dedbcd67a | ||
|
|
ff03bfa9cd | ||
|
|
b7d5a9876c | ||
|
|
96c482a1f5 |
@@ -1,4 +0,0 @@
|
||||
version: 3
|
||||
|
||||
project:
|
||||
id: github.com/replicatedhq/troubleshoot
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: "1.19"
|
||||
go-version: "1.20"
|
||||
|
||||
- name: setup env
|
||||
run: |
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
|
||||
shell: bash
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- run: make test
|
||||
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: "1.19"
|
||||
go-version: "1.20"
|
||||
|
||||
- name: setup env
|
||||
run: |
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
|
||||
shell: bash
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: replicatedhq/action-k3s@main
|
||||
id: k3s
|
||||
with:
|
||||
@@ -68,13 +68,13 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: "1.19"
|
||||
go-version: "1.20"
|
||||
- name: setup env
|
||||
run: |
|
||||
echo "GOPATH=$(go env GOPATH)" >> $GITHUB_ENV
|
||||
echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
|
||||
shell: bash
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- run: make check-schemas
|
||||
@@ -84,13 +84,13 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: "1.19"
|
||||
go-version: "1.20"
|
||||
- name: setup env
|
||||
run: |
|
||||
echo "GOPATH=$(go env GOPATH)" >> $GITHUB_ENV
|
||||
echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
|
||||
shell: bash
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- run: make generate preflight
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
@@ -149,7 +149,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
needs: compile-preflight
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: replicatedhq/action-k3s@main
|
||||
id: k3s
|
||||
with:
|
||||
@@ -165,7 +165,7 @@ jobs:
|
||||
run-examples:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- run: make run-examples
|
||||
|
||||
compile-supportbundle:
|
||||
@@ -173,13 +173,13 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: "1.19"
|
||||
go-version: "1.20"
|
||||
- name: setup env
|
||||
run: |
|
||||
echo "GOPATH=$(go env GOPATH)" >> $GITHUB_ENV
|
||||
echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
|
||||
shell: bash
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- run: make generate support-bundle
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
@@ -190,7 +190,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
needs: compile-supportbundle
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: replicatedhq/action-k3s@main
|
||||
id: k3s
|
||||
with:
|
||||
@@ -209,7 +209,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
needs: compile-supportbundle
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: replicatedhq/action-k3s@main
|
||||
id: k3s
|
||||
with:
|
||||
@@ -228,13 +228,13 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: "1.19"
|
||||
go-version: "1.20"
|
||||
- name: setup env
|
||||
run: |
|
||||
echo "GOPATH=$(go env GOPATH)" >> $GITHUB_ENV
|
||||
echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
|
||||
shell: bash
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Download support bundle binary
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
@@ -248,13 +248,13 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: "1.19"
|
||||
go-version: "1.20"
|
||||
- name: setup env
|
||||
run: |
|
||||
echo "GOPATH=$(go env GOPATH)" >> $GITHUB_ENV
|
||||
echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
|
||||
shell: bash
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- run: make generate collect
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
@@ -271,18 +271,20 @@ jobs:
|
||||
include:
|
||||
- goarch: arm
|
||||
goos: linux
|
||||
- goarch: riscv64
|
||||
goos: linux
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: "1.19"
|
||||
go-version: "1.20"
|
||||
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v4
|
||||
uses: goreleaser/goreleaser-action@v5
|
||||
with:
|
||||
version: "v0.183.0"
|
||||
args: build --rm-dist --snapshot --config deploy/.goreleaser.yaml --single-target
|
||||
@@ -299,7 +301,7 @@ jobs:
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -310,7 +312,7 @@ jobs:
|
||||
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: "1.19"
|
||||
go-version: "1.20"
|
||||
|
||||
- uses: sigstore/cosign-installer@v3
|
||||
with:
|
||||
@@ -331,7 +333,7 @@ jobs:
|
||||
COSIGN_KEY: ${{ secrets.COSIGN_KEY }}
|
||||
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v4
|
||||
uses: goreleaser/goreleaser-action@v5
|
||||
with:
|
||||
version: "v1.x" # Binary version to install
|
||||
args: release --rm-dist --config deploy/.goreleaser.yaml
|
||||
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run Trivy vulnerability scanner in repo mode
|
||||
uses: aquasecurity/trivy-action@master
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
name: FOSSA license scan
|
||||
|
||||
on:
|
||||
pull_request_target: # this is safe as these scans do not execute provided code
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- go.sum
|
||||
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
fossa-scan-pr:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request'
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: refs/pull/${{ github.event.number }}/merge
|
||||
- name: "Install FOSSA"
|
||||
uses: replicatedhq/action-fossa/install@main
|
||||
- name: "Run FOSSA Scan"
|
||||
uses: replicatedhq/action-fossa/scan@main
|
||||
with:
|
||||
api-key: ${{ secrets.FOSSA_API_KEY }}
|
||||
diff: true
|
||||
diff-ref: ${{ github.event.pull_request.base.sha }}
|
||||
debug: true
|
||||
|
||||
fossa-scan-merge:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push'
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 2
|
||||
- id: previous
|
||||
run: echo "sha=$(git rev-parse HEAD~1)" >> "${GITHUB_OUTPUT}"
|
||||
- name: "Install FOSSA"
|
||||
uses: replicatedhq/action-fossa/install@main
|
||||
- name: "Run FOSSA Scan"
|
||||
uses: replicatedhq/action-fossa/scan@main
|
||||
with:
|
||||
api-key: ${{ secrets.FOSSA_API_KEY }}
|
||||
diff: true
|
||||
diff-ref: ${{ steps.previous.outputs.sha }}
|
||||
debug: true
|
||||
@@ -0,0 +1,42 @@
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
TRIVY_VERSION: 0.44.1
|
||||
|
||||
name: License scan
|
||||
|
||||
jobs:
|
||||
license:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v4
|
||||
|
||||
- name: Install Go deps
|
||||
run: go mod download
|
||||
|
||||
- name: Install trivy
|
||||
run: |
|
||||
wget https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${TRIVY_VERSION}_Linux-64bit.deb
|
||||
sudo dpkg -i trivy_${TRIVY_VERSION}_Linux-64bit.deb
|
||||
|
||||
- name: Create license report artifact
|
||||
run: trivy fs --scanners license --skip-dirs ".github" . | tee license-report.txt
|
||||
|
||||
- name: Upload license report artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: license-report
|
||||
path: license-report.txt
|
||||
|
||||
- name: Check for unknown licenses
|
||||
run: trivy fs --scanners license --skip-dirs ".github" --exit-code 1 --severity UNKNOWN . || echo "::warning::Unknown licenses found, please verify"
|
||||
|
||||
- name: Check for forbidden licenses and fail
|
||||
run: trivy fs --scanners license --skip-dirs ".github" --exit-code 1 --severity CRITICAL,HIGH .
|
||||
@@ -7,7 +7,10 @@
|
||||
*.dylib
|
||||
bin
|
||||
.DS_Store
|
||||
|
||||
# npm generated files
|
||||
node_modules/
|
||||
package*.json
|
||||
|
||||
# Test binary, build with `go test -c`
|
||||
*.test
|
||||
|
||||
+7
-1
@@ -21,7 +21,7 @@ When implementing a new feature please review the [design principles](./docs/des
|
||||
|
||||
To get started we recommend:
|
||||
|
||||
1. Go (v1.19 or later)
|
||||
1. Go (v1.20 or later)
|
||||
2. A Kubernetes cluster (we recommend <https://k3d.io/>. This requires Docker v20.10.5 or later)
|
||||
3. Fork and clone the repo to $GOPATH/src/github.com/replicatedhq/
|
||||
4. Run `make support-bundle preflight` to generate binaries
|
||||
@@ -94,6 +94,12 @@ go tool pprof -http=":8000" cpu.prof
|
||||
go tool pprof -http=":8001" mem.prof
|
||||
```
|
||||
|
||||
**Additional flags for memory profiling**
|
||||
- `inuse_space`: Amount of memory allocated and not released yet (default).
|
||||
- `inuse_objects`: Amount of objects allocated and not released yet.
|
||||
- `alloc_space`: Total amount of memory allocated (regardless of released).
|
||||
- `alloc_objects`: Total amount of objects allocated (regardless of released).
|
||||
|
||||
More on profiling please visit https://go.dev/doc/diagnostics#profiling
|
||||
|
||||
## Contribution workflow
|
||||
|
||||
@@ -85,6 +85,9 @@ build:
|
||||
@echo "Build cli binaries"
|
||||
$(MAKE) -j support-bundle preflight analyze collect
|
||||
|
||||
mod-tidy:
|
||||
go mod tidy
|
||||
|
||||
.PHONY: support-bundle
|
||||
support-bundle:
|
||||
go build ${BUILDFLAGS} ${LDFLAGS} -o bin/support-bundle github.com/replicatedhq/troubleshoot/cmd/troubleshoot
|
||||
@@ -151,7 +154,7 @@ CONTROLLER_GEN=$(shell which controller-gen)
|
||||
.PHONY: client-gen
|
||||
client-gen:
|
||||
ifeq (, $(shell which client-gen 2>/dev/null))
|
||||
go install k8s.io/code-generator/cmd/client-gen@v0.26.1
|
||||
go install k8s.io/code-generator/cmd/client-gen@v0.28.2
|
||||
CLIENT_GEN=$(shell go env GOPATH)/bin/client-gen
|
||||
else
|
||||
CLIENT_GEN=$(shell which client-gen)
|
||||
@@ -252,4 +255,4 @@ watchrsync: npm-install
|
||||
.PHONY: npm-install
|
||||
npm-install:
|
||||
npm --version 2>&1 >/dev/null || ( echo "npm not installed; install npm to set up watchrsync" && exit 1 )
|
||||
npm list gaze-run-interrupt || npm install install gaze-run-interrupt@~2.0.0
|
||||
npm list gaze-run-interrupt || npm install install --no-save gaze-run-interrupt@~2.0.0
|
||||
|
||||
+1
-14
@@ -2,19 +2,6 @@
|
||||
|
||||
const gri = require('gaze-run-interrupt');
|
||||
|
||||
const binList = [
|
||||
// 'bin/analyze',
|
||||
// 'bin/preflight',
|
||||
'bin/support-bundle',
|
||||
// 'bin/collect'
|
||||
]
|
||||
const makeList = [
|
||||
// 'analyze',
|
||||
// 'preflight',
|
||||
'support-bundle',
|
||||
// 'collect'
|
||||
]
|
||||
|
||||
const commands = [
|
||||
// {
|
||||
// command: 'rm',
|
||||
@@ -22,7 +9,7 @@ const commands = [
|
||||
// },
|
||||
{
|
||||
command: 'make',
|
||||
args: makeList,
|
||||
args: ['build'],
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
+1
-7
@@ -16,12 +16,6 @@ const binList = [
|
||||
'bin/support-bundle',
|
||||
// 'bin/collect'
|
||||
]
|
||||
const makeList = [
|
||||
// 'analyze',
|
||||
// 'preflight',
|
||||
'support-bundle',
|
||||
// 'collect'
|
||||
]
|
||||
|
||||
const commands = [
|
||||
// {
|
||||
@@ -30,7 +24,7 @@ const commands = [
|
||||
// },
|
||||
{
|
||||
command: 'make',
|
||||
args: makeList,
|
||||
args: ['build'],
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/replicatedhq/troubleshoot/cmd/util"
|
||||
"github.com/replicatedhq/troubleshoot/cmd/internal/util"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/k8sutil"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/logger"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -43,6 +43,8 @@ func RootCmd() *cobra.Command {
|
||||
|
||||
cobra.OnInitialize(initConfig)
|
||||
|
||||
cmd.AddCommand(util.VersionCmd())
|
||||
|
||||
cmd.Flags().String("analyzers", "", "filename or url of the analyzers to use")
|
||||
cmd.Flags().Bool("debug", false, "enable debug logging")
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ func runAnalyzers(v *viper.Viper, bundlePath string) error {
|
||||
specContent := ""
|
||||
var err error
|
||||
if _, err = os.Stat(specPath); err == nil {
|
||||
b, err := ioutil.ReadFile(specPath)
|
||||
b, err := os.ReadFile(specPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -26,6 +26,7 @@ func runAnalyzers(v *viper.Viper, bundlePath string) error {
|
||||
specContent = string(b)
|
||||
} else {
|
||||
if !util.IsURL(specPath) {
|
||||
// TODO: Better error message when we do not have a file/url etc
|
||||
return fmt.Errorf("%s is not a URL and was not found (err %s)", specPath, err)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/replicatedhq/troubleshoot/cmd/util"
|
||||
"github.com/replicatedhq/troubleshoot/cmd/internal/util"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/k8sutil"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/logger"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -43,7 +43,7 @@ func RootCmd() *cobra.Command {
|
||||
|
||||
cobra.OnInitialize(initConfig)
|
||||
|
||||
cmd.AddCommand(VersionCmd())
|
||||
cmd.AddCommand(util.VersionCmd())
|
||||
|
||||
cmd.Flags().StringSlice("redactors", []string{}, "names of the additional redactors to use")
|
||||
cmd.Flags().Bool("redact", true, "enable/disable default redactions")
|
||||
|
||||
@@ -50,7 +50,7 @@ func runCollect(v *viper.Viper, arg string) error {
|
||||
|
||||
collectorContent = spec
|
||||
} else if _, err = os.Stat(arg); err == nil {
|
||||
b, err := ioutil.ReadFile(arg)
|
||||
b, err := os.ReadFile(arg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/replicatedhq/troubleshoot/pkg/version"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func VersionCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Print the current version and exit",
|
||||
Long: `Print the current version and exit`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
fmt.Printf("Replicated Collect %s\n", version.Version())
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package cli
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -2,9 +2,12 @@ package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/replicatedhq/troubleshoot/pkg/logger"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/oci"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func OciFetchCmd() *cobra.Command {
|
||||
@@ -12,6 +15,13 @@ func OciFetchCmd() *cobra.Command {
|
||||
Use: "oci-fetch [URI]",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Short: "Fetch a preflight from an OCI registry and print it to standard out",
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
v := viper.GetViper()
|
||||
v.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
|
||||
v.BindPFlags(cmd.Flags())
|
||||
|
||||
logger.SetupLogger(v)
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
uri := args[0]
|
||||
data, err := oci.PullPreflightFromOCI(uri)
|
||||
@@ -22,5 +32,9 @@ func OciFetchCmd() *cobra.Command {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// Initialize klog flags
|
||||
logger.InitKlogFlags(cmd)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/replicatedhq/troubleshoot/cmd/util"
|
||||
"github.com/replicatedhq/troubleshoot/cmd/internal/util"
|
||||
"github.com/replicatedhq/troubleshoot/internal/traces"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/constants"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/k8sutil"
|
||||
@@ -49,7 +49,7 @@ that a cluster meets the requirements to run an application.`,
|
||||
}
|
||||
|
||||
err = preflight.RunPreflights(v.GetBool("interactive"), v.GetString("output"), v.GetString("format"), args)
|
||||
if v.GetBool("debug") || v.IsSet("v") {
|
||||
if !v.GetBool("dry-run") && (v.GetBool("debug") || v.IsSet("v")) {
|
||||
fmt.Printf("\n%s", traces.GetExporterInstance().GetSummary())
|
||||
}
|
||||
|
||||
@@ -64,10 +64,14 @@ that a cluster meets the requirements to run an application.`,
|
||||
|
||||
cobra.OnInitialize(initConfig)
|
||||
|
||||
cmd.AddCommand(VersionCmd())
|
||||
cmd.AddCommand(util.VersionCmd())
|
||||
cmd.AddCommand(OciFetchCmd())
|
||||
preflight.AddFlags(cmd.PersistentFlags())
|
||||
|
||||
// Dry run flag should be in cmd.PersistentFlags() flags made available to all subcommands
|
||||
// Adding here to avoid that
|
||||
cmd.Flags().Bool("dry-run", false, "print the preflight spec without running preflight checks")
|
||||
|
||||
k8sutil.AddFlags(cmd.Flags())
|
||||
|
||||
// Initialize klog flags
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/replicatedhq/troubleshoot/pkg/version"
|
||||
)
|
||||
|
||||
func VersionCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Print the current version and exit",
|
||||
Long: `Print the current version and exit`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
fmt.Printf("Replicated Preflight %s\n", version.Version())
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/replicatedhq/troubleshoot/cmd/util"
|
||||
"github.com/replicatedhq/troubleshoot/cmd/internal/util"
|
||||
"github.com/replicatedhq/troubleshoot/internal/traces"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/k8sutil"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/logger"
|
||||
@@ -45,7 +45,7 @@ from a server that can be used to assist when troubleshooting a Kubernetes clust
|
||||
}
|
||||
|
||||
err = runTroubleshoot(v, args)
|
||||
if v.GetBool("debug") || v.IsSet("v") {
|
||||
if !v.IsSet("dry-run") && (v.GetBool("debug") || v.IsSet("v")) {
|
||||
fmt.Printf("\n%s", traces.GetExporterInstance().GetSummary())
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ from a server that can be used to assist when troubleshooting a Kubernetes clust
|
||||
|
||||
cmd.AddCommand(Analyze())
|
||||
cmd.AddCommand(Redact())
|
||||
cmd.AddCommand(VersionCmd())
|
||||
cmd.AddCommand(util.VersionCmd())
|
||||
|
||||
cmd.Flags().StringSlice("redactors", []string{}, "names of the additional redactors to use")
|
||||
cmd.Flags().Bool("redact", true, "enable/disable default redactions")
|
||||
@@ -74,6 +74,7 @@ from a server that can be used to assist when troubleshooting a Kubernetes clust
|
||||
cmd.Flags().String("since", "", "force pod logs collectors to return logs newer than a relative duration like 5s, 2m, or 3h.")
|
||||
cmd.Flags().StringP("output", "o", "", "specify the output file path for the support bundle")
|
||||
cmd.Flags().Bool("debug", false, "enable debug logging. This is equivalent to --v=0")
|
||||
cmd.Flags().Bool("dry-run", false, "print support bundle spec without collecting anything")
|
||||
|
||||
// hidden in favor of the `insecure-skip-tls-verify` flag
|
||||
cmd.Flags().Bool("allow-insecure-connections", false, "when set, do not verify TLS certs when retrieving spec and reporting results")
|
||||
@@ -81,7 +82,7 @@ from a server that can be used to assist when troubleshooting a Kubernetes clust
|
||||
|
||||
// `no-uri` references the `followURI` functionality where we can use an upstream spec when creating a support bundle
|
||||
// This flag makes sure we can also disable this and fall back to the default spec.
|
||||
cmd.Flags().Bool("no-uri", false, "When this flag is used, Troubleshoot does not attempt to retrieve the bundle referenced by the uri: field in the spec.`")
|
||||
cmd.Flags().Bool("no-uri", false, "When this flag is used, Troubleshoot does not attempt to retrieve the spec referenced by the uri: field`")
|
||||
|
||||
k8sutil.AddFlags(cmd.Flags())
|
||||
|
||||
|
||||
+130
-222
@@ -9,8 +9,6 @@ import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -18,29 +16,65 @@ import (
|
||||
"github.com/fatih/color"
|
||||
"github.com/mattn/go-isatty"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/replicatedhq/troubleshoot/internal/specs"
|
||||
"github.com/replicatedhq/troubleshoot/internal/util"
|
||||
analyzer "github.com/replicatedhq/troubleshoot/pkg/analyze"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/collect"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/constants"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/convert"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/httputil"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/k8sutil"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/specs"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/loader"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/supportbundle"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/types"
|
||||
"github.com/spf13/viper"
|
||||
spin "github.com/tj/go-spin"
|
||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
func runTroubleshoot(v *viper.Viper, arg []string) error {
|
||||
if !v.GetBool("load-cluster-specs") && len(arg) < 1 {
|
||||
func runTroubleshoot(v *viper.Viper, args []string) error {
|
||||
ctx := context.Background()
|
||||
if !v.GetBool("load-cluster-specs") && len(args) < 1 {
|
||||
return errors.New("flag load-cluster-specs must be set if no specs are provided on the command line")
|
||||
}
|
||||
|
||||
restConfig, err := k8sutil.GetRESTConfig()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to convert kube flags to rest config")
|
||||
}
|
||||
|
||||
client, err := kubernetes.NewForConfig(restConfig)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create kubernetes client")
|
||||
}
|
||||
|
||||
mainBundle, additionalRedactors, err := loadSpecs(ctx, args, client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// For --dry-run, we want to print the yaml and exit
|
||||
if v.GetBool("dry-run") {
|
||||
k := loader.TroubleshootKinds{
|
||||
SupportBundlesV1Beta2: []troubleshootv1beta2.SupportBundle{*mainBundle},
|
||||
}
|
||||
// If we have redactors, add them to the temp kinds object
|
||||
if len(additionalRedactors.Spec.Redactors) > 0 {
|
||||
k.RedactorsV1Beta2 = []troubleshootv1beta2.Redactor{*additionalRedactors}
|
||||
}
|
||||
|
||||
out, err := k.ToYaml()
|
||||
if err != nil {
|
||||
return types.NewExitCodeError(constants.EXIT_CODE_CATCH_ALL, errors.Wrap(err, "failed to convert specs to yaml"))
|
||||
}
|
||||
fmt.Printf("%s", out)
|
||||
return nil
|
||||
}
|
||||
|
||||
interactive := v.GetBool("interactive") && isatty.IsTerminal(os.Stdout.Fd())
|
||||
|
||||
if interactive {
|
||||
@@ -58,11 +92,6 @@ func runTroubleshoot(v *viper.Viper, arg []string) error {
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
restConfig, err := k8sutil.GetRESTConfig()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to convert kube flags to rest config")
|
||||
}
|
||||
|
||||
var sinceTime *time.Time
|
||||
if v.GetString("since-time") != "" || v.GetString("since") != "" {
|
||||
sinceTime, err = parseTimeFlags(v)
|
||||
@@ -77,61 +106,6 @@ func runTroubleshoot(v *viper.Viper, arg []string) error {
|
||||
})
|
||||
}
|
||||
|
||||
var mainBundle *troubleshootv1beta2.SupportBundle
|
||||
|
||||
additionalRedactors := &troubleshootv1beta2.Redactor{}
|
||||
|
||||
// Defining `v` below will render using `v` in reference to Viper unusable.
|
||||
// Therefore refactoring `v` to `val` will make sure we can still use it.
|
||||
for _, val := range arg {
|
||||
|
||||
collectorContent, err := supportbundle.LoadSupportBundleSpec(val)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to load support bundle spec")
|
||||
}
|
||||
multidocs := strings.Split(string(collectorContent), "\n---\n")
|
||||
// Referencing `ParseSupportBundle with a secondary arg of `no-uri`
|
||||
// Will make sure we can enable or disable the use of the `Spec.uri` field for an upstream spec.
|
||||
// This change will not have an impact on KOTS' usage of `ParseSupportBundle`
|
||||
// As Kots uses `load.go` directly.
|
||||
supportBundle, err := supportbundle.ParseSupportBundle([]byte(multidocs[0]), !v.GetBool("no-uri"))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to parse support bundle spec")
|
||||
}
|
||||
|
||||
mainBundle = supportbundle.ConcatSpec(mainBundle, supportBundle)
|
||||
|
||||
parsedRedactors, err := supportbundle.ParseRedactorsFromDocs(multidocs)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to parse redactors from doc")
|
||||
}
|
||||
additionalRedactors.Spec.Redactors = append(additionalRedactors.Spec.Redactors, parsedRedactors...)
|
||||
}
|
||||
|
||||
if v.GetBool("load-cluster-specs") {
|
||||
sbFromCluster, redactorsFromCluster, err := loadClusterSpecs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if sbFromCluster == nil {
|
||||
return errors.New("no specs found in cluster")
|
||||
}
|
||||
mainBundle = supportbundle.ConcatSpec(mainBundle, sbFromCluster)
|
||||
additionalRedactors.Spec.Redactors = append(additionalRedactors.Spec.Redactors, redactorsFromCluster.Spec.Redactors...)
|
||||
}
|
||||
|
||||
if mainBundle == nil {
|
||||
return errors.New("no support bundle specs provided to run")
|
||||
} else if mainBundle.Spec.Collectors == nil && mainBundle.Spec.HostCollectors == nil {
|
||||
return errors.New("no collectors specified in support bundle")
|
||||
}
|
||||
|
||||
redactors, err := supportbundle.GetRedactorsFromURIs(v.GetStringSlice("redactors"))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to get redactors")
|
||||
}
|
||||
additionalRedactors.Spec.Redactors = append(additionalRedactors.Spec.Redactors, redactors...)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
collectorCB := func(c chan interface{}, msg string) { c <- msg }
|
||||
progressChan := make(chan interface{})
|
||||
@@ -258,178 +232,112 @@ the %s Admin Console to begin analysis.`
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadClusterSpecs loads the support bundle and redactor specs from the cluster
|
||||
// based on the provided labels. By default this will be troubleshoot.io/kind=support-bundle and troubleshoot.sh/kind=support-bundle label selectors. We search for secrets
|
||||
// and configmaps with the label selector and parse the data as a support bundle. If the
|
||||
// user does not have sufficient permissions to list & read secrets and configmaps from
|
||||
// all namespaces, we will fallback to trying each namespace individually, and eventually
|
||||
// default to the configured kubeconfig namespace.
|
||||
func loadClusterSpecs() (*troubleshootv1beta2.SupportBundle, *troubleshootv1beta2.Redactor, error) {
|
||||
redactors := &troubleshootv1beta2.Redactor{}
|
||||
|
||||
v := viper.GetViper() // It's singleton, so we can use it anywhere
|
||||
|
||||
klog.Info("Discover troubleshoot specs from cluster")
|
||||
|
||||
selectors := v.GetStringSlice("selector")
|
||||
if reflect.DeepEqual(selectors, []string{"troubleshoot.sh/kind=support-bundle"}) {
|
||||
// Its the default selector so we append troubleshoot.io/kind=support-bundle to it due to backwards compatibility
|
||||
selectors = append(selectors, "troubleshoot.io/kind=support-bundle")
|
||||
}
|
||||
|
||||
labelSelector := strings.Join(selectors, ",")
|
||||
|
||||
parsedSelector, err := labels.Parse(labelSelector)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "unable to parse selector")
|
||||
}
|
||||
|
||||
config, err := k8sutil.GetRESTConfig()
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "failed to convert kube flags to rest config")
|
||||
}
|
||||
|
||||
client, err := kubernetes.NewForConfig(config)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "failed to convert create k8s client")
|
||||
}
|
||||
|
||||
// List of namespaces we want to search for secrets and configmaps with support bundle specs
|
||||
namespaces := []string{}
|
||||
ctx := context.Background()
|
||||
|
||||
if v.GetString("namespace") != "" {
|
||||
// Just progress with the namespace provided
|
||||
namespaces = []string{v.GetString("namespace")}
|
||||
} else {
|
||||
// Check if I can read secrets and configmaps in all namespaces
|
||||
ican, err := k8sutil.CanIListAndGetAllSecretsAndConfigMaps(ctx, client)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "failed to check if I can read secrets and configmaps")
|
||||
// loadSupportBundleSpecsFromURIs loads support bundle specs from URIs
|
||||
func loadSupportBundleSpecsFromURIs(ctx context.Context, kinds *loader.TroubleshootKinds) (*loader.TroubleshootKinds, error) {
|
||||
remoteRawSpecs := []string{}
|
||||
for _, s := range kinds.SupportBundlesV1Beta2 {
|
||||
if s.Spec.Uri != "" && util.IsURL(s.Spec.Uri) {
|
||||
// We are using LoadSupportBundleSpec function here since it handles prompting
|
||||
// users to accept insecure connections
|
||||
// There is an opportunity to refactor this code in favour of the Loader APIs
|
||||
// TODO: Pass ctx to LoadSupportBundleSpec
|
||||
rawSpec, err := supportbundle.LoadSupportBundleSpec(s.Spec.Uri)
|
||||
if err != nil {
|
||||
// In the event a spec can't be loaded, we'll just skip it and print a warning
|
||||
klog.Warningf("unable to load support bundle from URI: %q: %v", s.Spec.Uri, err)
|
||||
continue
|
||||
}
|
||||
remoteRawSpecs = append(remoteRawSpecs, string(rawSpec))
|
||||
}
|
||||
klog.V(1).Infof("Can I read any secrets and configmaps: %v", ican)
|
||||
}
|
||||
|
||||
if ican {
|
||||
// I can read secrets and configmaps in all namespaces
|
||||
// No need to iterate over all namespaces
|
||||
namespaces = []string{""}
|
||||
if len(remoteRawSpecs) == 0 {
|
||||
return kinds, nil
|
||||
}
|
||||
|
||||
return loader.LoadSpecs(ctx, loader.LoadOptions{
|
||||
RawSpecs: remoteRawSpecs,
|
||||
})
|
||||
}
|
||||
|
||||
func loadSpecs(ctx context.Context, args []string, client kubernetes.Interface) (*troubleshootv1beta2.SupportBundle, *troubleshootv1beta2.Redactor, error) {
|
||||
// Append redactor uris to the args
|
||||
allArgs := append(args, viper.GetStringSlice("redactors")...)
|
||||
kinds, err := specs.LoadFromCLIArgs(ctx, client, allArgs, viper.GetViper())
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Load additional specs from support bundle URIs
|
||||
if !viper.GetBool("no-uri") {
|
||||
moreKinds, err := loadSupportBundleSpecsFromURIs(ctx, kinds)
|
||||
if err != nil {
|
||||
klog.Warningf("unable to load support bundles from URIs: %v", err)
|
||||
} else {
|
||||
// Get list of all namespaces and try to find specs from each namespace
|
||||
nsList, err := client.CoreV1().Namespaces().List(ctx, metav1.ListOptions{})
|
||||
if err != nil {
|
||||
if k8serrors.IsForbidden(err) {
|
||||
kubeconfig := k8sutil.GetKubeconfig()
|
||||
ns, _, err := kubeconfig.Namespace()
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "failed to get namespace from kubeconfig")
|
||||
}
|
||||
// If we are not allowed to list namespaces, just use the default namespace
|
||||
// configured in the kubeconfig
|
||||
namespaces = []string{ns}
|
||||
} else {
|
||||
return nil, nil, errors.Wrap(err, "failed to list namespaces")
|
||||
}
|
||||
}
|
||||
|
||||
for _, ns := range nsList.Items {
|
||||
namespaces = append(namespaces, ns.Name)
|
||||
}
|
||||
kinds.Add(moreKinds)
|
||||
}
|
||||
}
|
||||
|
||||
var bundlesFromCluster []string
|
||||
|
||||
parsedSelectorStrings, err := specs.SplitTroubleshootSecretLabelSelector(client, parsedSelector)
|
||||
if err != nil {
|
||||
klog.Errorf("failed to parse troubleshoot labels selector %s", err)
|
||||
// Check if we have any collectors to run in the troubleshoot specs
|
||||
// TODO: Do we use the RemoteCollectors anymore?
|
||||
if len(kinds.CollectorsV1Beta2) == 0 &&
|
||||
len(kinds.HostCollectorsV1Beta2) == 0 &&
|
||||
len(kinds.SupportBundlesV1Beta2) == 0 {
|
||||
return nil, nil, errors.New("no collectors specified to run")
|
||||
}
|
||||
|
||||
// Search cluster for support bundle specs
|
||||
for _, parsedSelectorString := range parsedSelectorStrings {
|
||||
klog.V(1).Infof("Search support bundle specs from [%q] namespace using %q selector", strings.Join(namespaces, ", "), parsedSelectorString)
|
||||
for _, ns := range namespaces {
|
||||
bundlesFromSecrets, err := specs.LoadFromSecretMatchingLabel(client, parsedSelectorString, ns, constants.SupportBundleKey)
|
||||
if err != nil {
|
||||
if !k8serrors.IsForbidden(err) {
|
||||
klog.Errorf("failed to load support bundle spec from secrets: %s", err)
|
||||
} else {
|
||||
klog.Warningf("Reading secrets from %q namespace forbidden", ns)
|
||||
}
|
||||
}
|
||||
bundlesFromCluster = append(bundlesFromCluster, bundlesFromSecrets...)
|
||||
|
||||
bundlesFromConfigMaps, err := specs.LoadFromConfigMapMatchingLabel(client, parsedSelectorString, ns, constants.SupportBundleKey)
|
||||
if err != nil {
|
||||
if !k8serrors.IsForbidden(err) {
|
||||
klog.Errorf("failed to load support bundle spec from configmap: %s", err)
|
||||
} else {
|
||||
klog.Warningf("Reading configmaps from %q namespace forbidden", ns)
|
||||
}
|
||||
}
|
||||
bundlesFromCluster = append(bundlesFromCluster, bundlesFromConfigMaps...)
|
||||
}
|
||||
// Merge specs
|
||||
// We need to add the default type information to the support bundle spec
|
||||
// since by default these fields would be empty
|
||||
mainBundle := &troubleshootv1beta2.SupportBundle{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "troubleshoot.sh/v1beta2",
|
||||
Kind: "SupportBundle",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "merged-support-bundle-spec",
|
||||
},
|
||||
}
|
||||
for _, sb := range kinds.SupportBundlesV1Beta2 {
|
||||
sb := sb
|
||||
mainBundle = supportbundle.ConcatSpec(mainBundle, &sb)
|
||||
}
|
||||
|
||||
parsedBundle := &troubleshootv1beta2.SupportBundle{}
|
||||
|
||||
for _, bundle := range bundlesFromCluster {
|
||||
multidocs := strings.Split(string(bundle), "\n---\n")
|
||||
bundleFromDoc, err := supportbundle.ParseSupportBundleFromDoc([]byte(multidocs[0]))
|
||||
if err != nil {
|
||||
klog.Errorf("failed to parse support bundle spec: %s", err)
|
||||
continue
|
||||
}
|
||||
|
||||
parsedBundle = supportbundle.ConcatSpec(parsedBundle, bundleFromDoc)
|
||||
|
||||
parsedRedactors, err := supportbundle.ParseRedactorsFromDocs(multidocs)
|
||||
if err != nil {
|
||||
klog.Errorf("failed to parse redactors from doc: %s", err)
|
||||
continue
|
||||
}
|
||||
|
||||
redactors.Spec.Redactors = append(redactors.Spec.Redactors, parsedRedactors...)
|
||||
for _, c := range kinds.CollectorsV1Beta2 {
|
||||
mainBundle.Spec.Collectors = append(mainBundle.Spec.Collectors, c.Spec.Collectors...)
|
||||
}
|
||||
|
||||
var redactorsFromCluster []string
|
||||
|
||||
// Search cluster for redactor specs
|
||||
for _, parsedSelectorString := range parsedSelectorStrings {
|
||||
klog.V(1).Infof("Search redactor specs from [%q] namespace using %q selector", strings.Join(namespaces, ", "), parsedSelectorString)
|
||||
for _, ns := range namespaces {
|
||||
redactorsFromSecrets, err := specs.LoadFromSecretMatchingLabel(client, parsedSelectorString, ns, constants.RedactorKey)
|
||||
if err != nil {
|
||||
if !k8serrors.IsForbidden(err) {
|
||||
klog.Errorf("failed to load support bundle spec from secrets: %s", err)
|
||||
} else {
|
||||
klog.Warningf("Reading secrets from %q namespace forbidden", ns)
|
||||
}
|
||||
}
|
||||
redactorsFromCluster = append(redactorsFromCluster, redactorsFromSecrets...)
|
||||
|
||||
redactorsFromConfigMaps, err := specs.LoadFromConfigMapMatchingLabel(client, parsedSelectorString, ns, constants.RedactorKey)
|
||||
if err != nil {
|
||||
if !k8serrors.IsForbidden(err) {
|
||||
klog.Errorf("failed to load support bundle spec from configmap: %s", err)
|
||||
} else {
|
||||
klog.Warningf("Reading configmaps from %q namespace forbidden", ns)
|
||||
}
|
||||
}
|
||||
redactorsFromCluster = append(redactorsFromCluster, redactorsFromConfigMaps...)
|
||||
}
|
||||
for _, hc := range kinds.HostCollectorsV1Beta2 {
|
||||
mainBundle.Spec.HostCollectors = append(mainBundle.Spec.HostCollectors, hc.Spec.Collectors...)
|
||||
}
|
||||
|
||||
for _, redactor := range redactorsFromCluster {
|
||||
multidocs := strings.Split(string(redactor), "\n---\n")
|
||||
parsedRedactors, err := supportbundle.ParseRedactorsFromDocs(multidocs)
|
||||
if err != nil {
|
||||
klog.Errorf("failed to parse redactors from doc: %s", err)
|
||||
}
|
||||
// Ensure cluster info and cluster resources collectors are in the merged spec
|
||||
// We need to add them here so when we --dry-run, these collectors are included.
|
||||
// supportbundle.runCollectors duplicates this bit. We'll need to refactor it out later
|
||||
// when its clearer what other code depends on this logic e.g KOTS
|
||||
mainBundle.Spec.Collectors = collect.EnsureCollectorInList(
|
||||
mainBundle.Spec.Collectors,
|
||||
troubleshootv1beta2.Collect{ClusterInfo: &troubleshootv1beta2.ClusterInfo{}},
|
||||
)
|
||||
mainBundle.Spec.Collectors = collect.EnsureCollectorInList(
|
||||
mainBundle.Spec.Collectors,
|
||||
troubleshootv1beta2.Collect{ClusterResources: &troubleshootv1beta2.ClusterResources{}},
|
||||
)
|
||||
|
||||
redactors.Spec.Redactors = append(redactors.Spec.Redactors, parsedRedactors...)
|
||||
additionalRedactors := &troubleshootv1beta2.Redactor{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "troubleshoot.sh/v1beta2",
|
||||
Kind: "Redactor",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "merged-redactors-spec",
|
||||
},
|
||||
}
|
||||
for _, r := range kinds.RedactorsV1Beta2 {
|
||||
additionalRedactors.Spec.Redactors = append(additionalRedactors.Spec.Redactors, r.Spec.Redactors...)
|
||||
}
|
||||
|
||||
return parsedBundle, redactors, nil
|
||||
return mainBundle, additionalRedactors, nil
|
||||
}
|
||||
|
||||
func parseTimeFlags(v *viper.Viper) (*time.Time, error) {
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/replicatedhq/troubleshoot/pkg/httputil"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/loader"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var orig = `
|
||||
apiVersion: troubleshoot.sh/v1beta2
|
||||
kind: SupportBundle
|
||||
metadata:
|
||||
name: sb-1
|
||||
spec:
|
||||
uri: $MY_URI
|
||||
collectors:
|
||||
- configMap:
|
||||
name: kube-root-ca.crt
|
||||
namespace: default
|
||||
`
|
||||
|
||||
func Test_loadSupportBundleSpecsFromURIs(t *testing.T) {
|
||||
// Run a webserver to serve the spec
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte(`
|
||||
apiVersion: troubleshoot.sh/v1beta2
|
||||
kind: SupportBundle
|
||||
metadata:
|
||||
name: sb-2
|
||||
spec:
|
||||
collectors:
|
||||
- clusterInfo: {}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
orig := strings.ReplaceAll(orig, "$MY_URI", srv.URL)
|
||||
|
||||
ctx := context.Background()
|
||||
kinds, err := loader.LoadSpecs(ctx, loader.LoadOptions{RawSpec: orig})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, kinds)
|
||||
|
||||
moreKinds, err := loadSupportBundleSpecsFromURIs(ctx, kinds)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, moreKinds.SupportBundlesV1Beta2, 1)
|
||||
assert.NotNil(t, moreKinds.SupportBundlesV1Beta2[0].Spec.Collectors[0].ClusterInfo)
|
||||
}
|
||||
|
||||
func Test_loadSupportBundleSpecsFromURIs_TimeoutError(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(2 * time.Second) // this will cause a timeout
|
||||
}))
|
||||
defer srv.Close()
|
||||
ctx := context.Background()
|
||||
|
||||
kinds, err := loader.LoadSpecs(ctx, loader.LoadOptions{
|
||||
RawSpec: strings.ReplaceAll(orig, "$MY_URI", srv.URL),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Set the timeout on the http client to 10ms
|
||||
// supportbundle.LoadSupportBundleSpec does not yet use the context
|
||||
before := httputil.GetHttpClient().Timeout
|
||||
httputil.GetHttpClient().Timeout = 10 * time.Millisecond
|
||||
defer func() {
|
||||
// Reinstate the original timeout. Its a global var so we need to reset it
|
||||
httputil.GetHttpClient().Timeout = before
|
||||
}()
|
||||
|
||||
kindsAfter, err := loadSupportBundleSpecsFromURIs(ctx, kinds)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, kinds, kindsAfter)
|
||||
}
|
||||
@@ -163,6 +163,13 @@ spec:
|
||||
type: object
|
||||
insecureSkipVerify:
|
||||
type: boolean
|
||||
timeout:
|
||||
description: A Duration represents the elapsed time
|
||||
between two instants as an int64 nanosecond count.
|
||||
The representation limits the largest representable
|
||||
duration to approximately 290 years.
|
||||
format: int64
|
||||
type: integer
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
@@ -180,6 +187,13 @@ spec:
|
||||
type: object
|
||||
insecureSkipVerify:
|
||||
type: boolean
|
||||
timeout:
|
||||
description: A Duration represents the elapsed time
|
||||
between two instants as an int64 nanosecond count.
|
||||
The representation limits the largest representable
|
||||
duration to approximately 290 years.
|
||||
format: int64
|
||||
type: integer
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
@@ -195,6 +209,13 @@ spec:
|
||||
type: object
|
||||
insecureSkipVerify:
|
||||
type: boolean
|
||||
timeout:
|
||||
description: A Duration represents the elapsed time
|
||||
between two instants as an int64 nanosecond count.
|
||||
The representation limits the largest representable
|
||||
duration to approximately 290 years.
|
||||
format: int64
|
||||
type: integer
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
|
||||
@@ -835,6 +835,13 @@ spec:
|
||||
type: object
|
||||
insecureSkipVerify:
|
||||
type: boolean
|
||||
timeout:
|
||||
description: A Duration represents the elapsed time
|
||||
between two instants as an int64 nanosecond count.
|
||||
The representation limits the largest representable
|
||||
duration to approximately 290 years.
|
||||
format: int64
|
||||
type: integer
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
@@ -852,6 +859,13 @@ spec:
|
||||
type: object
|
||||
insecureSkipVerify:
|
||||
type: boolean
|
||||
timeout:
|
||||
description: A Duration represents the elapsed time
|
||||
between two instants as an int64 nanosecond count.
|
||||
The representation limits the largest representable
|
||||
duration to approximately 290 years.
|
||||
format: int64
|
||||
type: integer
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
@@ -867,6 +881,13 @@ spec:
|
||||
type: object
|
||||
insecureSkipVerify:
|
||||
type: boolean
|
||||
timeout:
|
||||
description: A Duration represents the elapsed time
|
||||
between two instants as an int64 nanosecond count.
|
||||
The representation limits the largest representable
|
||||
duration to approximately 290 years.
|
||||
format: int64
|
||||
type: integer
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
|
||||
@@ -866,6 +866,13 @@ spec:
|
||||
type: object
|
||||
insecureSkipVerify:
|
||||
type: boolean
|
||||
timeout:
|
||||
description: A Duration represents the elapsed time
|
||||
between two instants as an int64 nanosecond count.
|
||||
The representation limits the largest representable
|
||||
duration to approximately 290 years.
|
||||
format: int64
|
||||
type: integer
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
@@ -883,6 +890,13 @@ spec:
|
||||
type: object
|
||||
insecureSkipVerify:
|
||||
type: boolean
|
||||
timeout:
|
||||
description: A Duration represents the elapsed time
|
||||
between two instants as an int64 nanosecond count.
|
||||
The representation limits the largest representable
|
||||
duration to approximately 290 years.
|
||||
format: int64
|
||||
type: integer
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
@@ -898,6 +912,13 @@ spec:
|
||||
type: object
|
||||
insecureSkipVerify:
|
||||
type: boolean
|
||||
timeout:
|
||||
description: A Duration represents the elapsed time
|
||||
between two instants as an int64 nanosecond count.
|
||||
The representation limits the largest representable
|
||||
duration to approximately 290 years.
|
||||
format: int64
|
||||
type: integer
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
|
||||
@@ -1489,6 +1489,19 @@ spec:
|
||||
required:
|
||||
- outcomes
|
||||
type: object
|
||||
velero:
|
||||
properties:
|
||||
annotations:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
checkName:
|
||||
type: string
|
||||
exclude:
|
||||
type: BoolString
|
||||
strict:
|
||||
type: BoolString
|
||||
type: object
|
||||
weaveReport:
|
||||
properties:
|
||||
annotations:
|
||||
|
||||
@@ -318,6 +318,17 @@ spec:
|
||||
- namespace
|
||||
- selector
|
||||
type: object
|
||||
helm:
|
||||
properties:
|
||||
collectorName:
|
||||
type: string
|
||||
exclude:
|
||||
type: BoolString
|
||||
namespace:
|
||||
type: string
|
||||
releaseName:
|
||||
type: string
|
||||
type: object
|
||||
http:
|
||||
properties:
|
||||
collectorName:
|
||||
@@ -332,6 +343,11 @@ spec:
|
||||
type: object
|
||||
insecureSkipVerify:
|
||||
type: boolean
|
||||
timeout:
|
||||
description: Timeout is the time to wait for a server's
|
||||
response. Its a duration e.g 15s, 2h30m. Missing value
|
||||
or empty string or means no timeout.
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
@@ -349,6 +365,11 @@ spec:
|
||||
type: object
|
||||
insecureSkipVerify:
|
||||
type: boolean
|
||||
timeout:
|
||||
description: Timeout is the time to wait for a server's
|
||||
response. Its a duration e.g 15s, 2h30m. Missing value
|
||||
or empty string or means no timeout.
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
@@ -364,6 +385,11 @@ spec:
|
||||
type: object
|
||||
insecureSkipVerify:
|
||||
type: boolean
|
||||
timeout:
|
||||
description: Timeout is the time to wait for a server's
|
||||
response. Its a duration e.g 15s, 2h30m. Missing value
|
||||
or empty string or means no timeout.
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
@@ -2600,6 +2626,31 @@ spec:
|
||||
info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/'
|
||||
type: object
|
||||
type: object
|
||||
restartPolicy:
|
||||
description: 'RestartPolicy defines the restart
|
||||
behavior of individual containers in a pod.
|
||||
This field may only be set for init containers,
|
||||
and the only allowed value is "Always". For
|
||||
non-init containers or when this field is not
|
||||
specified, the restart behavior is defined by
|
||||
the Pod''s restart policy and the container
|
||||
type. Setting the RestartPolicy as "Always"
|
||||
for the init container will have the following
|
||||
effect: this init container will be continually
|
||||
restarted on exit until all regular containers
|
||||
have terminated. Once all regular containers
|
||||
have completed, all init containers with restartPolicy
|
||||
"Always" will be shut down. This lifecycle differs
|
||||
from normal init containers and is often referred
|
||||
to as a "sidecar" container. Although this init
|
||||
container still starts in the init container
|
||||
sequence, it does not wait for the container
|
||||
to complete before proceeding to the next init
|
||||
container. Instead, the next init container
|
||||
starts immediately after this init container
|
||||
is started, or after any startupProbe has successfully
|
||||
completed.'
|
||||
type: string
|
||||
securityContext:
|
||||
description: 'SecurityContext defines the security
|
||||
options the container should be run with. If
|
||||
@@ -2739,8 +2790,8 @@ spec:
|
||||
preconfigured on the node to work. Must
|
||||
be a descending path, relative to the
|
||||
kubelet's configured seccomp profile
|
||||
location. Must only be set if type is
|
||||
"Localhost".
|
||||
location. Must be set if type is "Localhost".
|
||||
Must NOT be set for any other type.
|
||||
type: string
|
||||
type:
|
||||
description: "type indicates which kind
|
||||
@@ -2780,18 +2831,13 @@ spec:
|
||||
hostProcess:
|
||||
description: HostProcess determines if
|
||||
a container should be run as a 'Host
|
||||
Process' container. This field is alpha-level
|
||||
and will only be honored by components
|
||||
that enable the WindowsHostProcessContainers
|
||||
feature flag. Setting this field without
|
||||
the feature flag will result in errors
|
||||
when validating the Pod. All of a Pod's
|
||||
containers must have the same effective
|
||||
HostProcess value (it is not allowed
|
||||
to have a mix of HostProcess containers
|
||||
and non-HostProcess containers). In
|
||||
addition, if HostProcess is true then
|
||||
HostNetwork must also be set to true.
|
||||
Process' container. All of a Pod's containers
|
||||
must have the same effective HostProcess
|
||||
value (it is not allowed to have a mix
|
||||
of HostProcess containers and non-HostProcess
|
||||
containers). In addition, if HostProcess
|
||||
is true then HostNetwork must also be
|
||||
set to true.
|
||||
type: boolean
|
||||
runAsUserName:
|
||||
description: The UserName in Windows to
|
||||
@@ -4110,6 +4156,13 @@ spec:
|
||||
info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/'
|
||||
type: object
|
||||
type: object
|
||||
restartPolicy:
|
||||
description: Restart policy for the container
|
||||
to manage the restart behavior of each container
|
||||
within a pod. This may only be set for init
|
||||
containers. You cannot set this field on ephemeral
|
||||
containers.
|
||||
type: string
|
||||
securityContext:
|
||||
description: 'Optional: SecurityContext defines
|
||||
the security options the ephemeral container
|
||||
@@ -4248,8 +4301,8 @@ spec:
|
||||
preconfigured on the node to work. Must
|
||||
be a descending path, relative to the
|
||||
kubelet's configured seccomp profile
|
||||
location. Must only be set if type is
|
||||
"Localhost".
|
||||
location. Must be set if type is "Localhost".
|
||||
Must NOT be set for any other type.
|
||||
type: string
|
||||
type:
|
||||
description: "type indicates which kind
|
||||
@@ -4289,18 +4342,13 @@ spec:
|
||||
hostProcess:
|
||||
description: HostProcess determines if
|
||||
a container should be run as a 'Host
|
||||
Process' container. This field is alpha-level
|
||||
and will only be honored by components
|
||||
that enable the WindowsHostProcessContainers
|
||||
feature flag. Setting this field without
|
||||
the feature flag will result in errors
|
||||
when validating the Pod. All of a Pod's
|
||||
containers must have the same effective
|
||||
HostProcess value (it is not allowed
|
||||
to have a mix of HostProcess containers
|
||||
and non-HostProcess containers). In
|
||||
addition, if HostProcess is true then
|
||||
HostNetwork must also be set to true.
|
||||
Process' container. All of a Pod's containers
|
||||
must have the same effective HostProcess
|
||||
value (it is not allowed to have a mix
|
||||
of HostProcess containers and non-HostProcess
|
||||
containers). In addition, if HostProcess
|
||||
is true then HostNetwork must also be
|
||||
set to true.
|
||||
type: boolean
|
||||
runAsUserName:
|
||||
description: The UserName in Windows to
|
||||
@@ -5656,6 +5704,31 @@ spec:
|
||||
info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/'
|
||||
type: object
|
||||
type: object
|
||||
restartPolicy:
|
||||
description: 'RestartPolicy defines the restart
|
||||
behavior of individual containers in a pod.
|
||||
This field may only be set for init containers,
|
||||
and the only allowed value is "Always". For
|
||||
non-init containers or when this field is not
|
||||
specified, the restart behavior is defined by
|
||||
the Pod''s restart policy and the container
|
||||
type. Setting the RestartPolicy as "Always"
|
||||
for the init container will have the following
|
||||
effect: this init container will be continually
|
||||
restarted on exit until all regular containers
|
||||
have terminated. Once all regular containers
|
||||
have completed, all init containers with restartPolicy
|
||||
"Always" will be shut down. This lifecycle differs
|
||||
from normal init containers and is often referred
|
||||
to as a "sidecar" container. Although this init
|
||||
container still starts in the init container
|
||||
sequence, it does not wait for the container
|
||||
to complete before proceeding to the next init
|
||||
container. Instead, the next init container
|
||||
starts immediately after this init container
|
||||
is started, or after any startupProbe has successfully
|
||||
completed.'
|
||||
type: string
|
||||
securityContext:
|
||||
description: 'SecurityContext defines the security
|
||||
options the container should be run with. If
|
||||
@@ -5795,8 +5868,8 @@ spec:
|
||||
preconfigured on the node to work. Must
|
||||
be a descending path, relative to the
|
||||
kubelet's configured seccomp profile
|
||||
location. Must only be set if type is
|
||||
"Localhost".
|
||||
location. Must be set if type is "Localhost".
|
||||
Must NOT be set for any other type.
|
||||
type: string
|
||||
type:
|
||||
description: "type indicates which kind
|
||||
@@ -5836,18 +5909,13 @@ spec:
|
||||
hostProcess:
|
||||
description: HostProcess determines if
|
||||
a container should be run as a 'Host
|
||||
Process' container. This field is alpha-level
|
||||
and will only be honored by components
|
||||
that enable the WindowsHostProcessContainers
|
||||
feature flag. Setting this field without
|
||||
the feature flag will result in errors
|
||||
when validating the Pod. All of a Pod's
|
||||
containers must have the same effective
|
||||
HostProcess value (it is not allowed
|
||||
to have a mix of HostProcess containers
|
||||
and non-HostProcess containers). In
|
||||
addition, if HostProcess is true then
|
||||
HostNetwork must also be set to true.
|
||||
Process' container. All of a Pod's containers
|
||||
must have the same effective HostProcess
|
||||
value (it is not allowed to have a mix
|
||||
of HostProcess containers and non-HostProcess
|
||||
containers). In addition, if HostProcess
|
||||
is true then HostNetwork must also be
|
||||
set to true.
|
||||
type: boolean
|
||||
runAsUserName:
|
||||
description: The UserName in Windows to
|
||||
@@ -6309,17 +6377,10 @@ spec:
|
||||
template will be used to create a new ResourceClaim,
|
||||
which will be bound to this pod. When this
|
||||
pod is deleted, the ResourceClaim will also
|
||||
be deleted. The name of the ResourceClaim
|
||||
will be <pod name>-<resource name>, where
|
||||
<resource name> is the PodResourceClaim.Name.
|
||||
Pod validation will reject the pod if the
|
||||
concatenated name is not valid for a ResourceClaim
|
||||
(e.g. too long). \n An existing ResourceClaim
|
||||
with that name that is not owned by the
|
||||
pod will not be used for the pod to avoid
|
||||
using an unrelated resource by mistake.
|
||||
Scheduling and pod startup are then blocked
|
||||
until the unrelated ResourceClaim is removed.
|
||||
be deleted. The pod name and resource name,
|
||||
along with a generated component, will be
|
||||
used to form a unique name for the ResourceClaim,
|
||||
which will be recorded in pod.status.resourceClaimStatuses.
|
||||
\n This field is immutable and no changes
|
||||
will be made to the corresponding ResourceClaim
|
||||
by the control plane after creating the
|
||||
@@ -6480,7 +6541,8 @@ spec:
|
||||
The profile must be preconfigured on the node
|
||||
to work. Must be a descending path, relative
|
||||
to the kubelet's configured seccomp profile
|
||||
location. Must only be set if type is "Localhost".
|
||||
location. Must be set if type is "Localhost".
|
||||
Must NOT be set for any other type.
|
||||
type: string
|
||||
type:
|
||||
description: "type indicates which kind of seccomp
|
||||
@@ -6553,14 +6615,10 @@ spec:
|
||||
hostProcess:
|
||||
description: HostProcess determines if a container
|
||||
should be run as a 'Host Process' container.
|
||||
This field is alpha-level and will only be
|
||||
honored by components that enable the WindowsHostProcessContainers
|
||||
feature flag. Setting this field without the
|
||||
feature flag will result in errors when validating
|
||||
the Pod. All of a Pod's containers must have
|
||||
the same effective HostProcess value (it is
|
||||
not allowed to have a mix of HostProcess containers
|
||||
and non-HostProcess containers). In addition,
|
||||
All of a Pod's containers must have the same
|
||||
effective HostProcess value (it is not allowed
|
||||
to have a mix of HostProcess containers and
|
||||
non-HostProcess containers). In addition,
|
||||
if HostProcess is true then HostNetwork must
|
||||
also be set to true.
|
||||
type: boolean
|
||||
|
||||
@@ -1260,6 +1260,11 @@ spec:
|
||||
type: object
|
||||
insecureSkipVerify:
|
||||
type: boolean
|
||||
timeout:
|
||||
description: Timeout is the time to wait for a server's
|
||||
response. Its a duration e.g 15s, 2h30m. Missing value
|
||||
or empty string or means no timeout.
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
@@ -1275,6 +1280,11 @@ spec:
|
||||
type: object
|
||||
insecureSkipVerify:
|
||||
type: boolean
|
||||
timeout:
|
||||
description: Timeout is the time to wait for a server's
|
||||
response. Its a duration e.g 15s, 2h30m. Missing value
|
||||
or empty string or means no timeout.
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
@@ -1290,6 +1300,11 @@ spec:
|
||||
type: object
|
||||
insecureSkipVerify:
|
||||
type: boolean
|
||||
timeout:
|
||||
description: Timeout is the time to wait for a server's
|
||||
response. Its a duration e.g 15s, 2h30m. Missing value
|
||||
or empty string or means no timeout.
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
@@ -1353,8 +1368,24 @@ spec:
|
||||
type: string
|
||||
command:
|
||||
type: string
|
||||
env:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
exclude:
|
||||
type: BoolString
|
||||
ignoreParentEnvs:
|
||||
type: boolean
|
||||
inheritEnvs:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
input:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
outputDir:
|
||||
type: string
|
||||
required:
|
||||
- args
|
||||
- command
|
||||
|
||||
@@ -1260,6 +1260,11 @@ spec:
|
||||
type: object
|
||||
insecureSkipVerify:
|
||||
type: boolean
|
||||
timeout:
|
||||
description: Timeout is the time to wait for a server's
|
||||
response. Its a duration e.g 15s, 2h30m. Missing value
|
||||
or empty string or means no timeout.
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
@@ -1275,6 +1280,11 @@ spec:
|
||||
type: object
|
||||
insecureSkipVerify:
|
||||
type: boolean
|
||||
timeout:
|
||||
description: Timeout is the time to wait for a server's
|
||||
response. Its a duration e.g 15s, 2h30m. Missing value
|
||||
or empty string or means no timeout.
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
@@ -1290,6 +1300,11 @@ spec:
|
||||
type: object
|
||||
insecureSkipVerify:
|
||||
type: boolean
|
||||
timeout:
|
||||
description: Timeout is the time to wait for a server's
|
||||
response. Its a duration e.g 15s, 2h30m. Missing value
|
||||
or empty string or means no timeout.
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
@@ -1353,8 +1368,24 @@ spec:
|
||||
type: string
|
||||
command:
|
||||
type: string
|
||||
env:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
exclude:
|
||||
type: BoolString
|
||||
ignoreParentEnvs:
|
||||
type: boolean
|
||||
inheritEnvs:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
input:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
outputDir:
|
||||
type: string
|
||||
required:
|
||||
- args
|
||||
- command
|
||||
@@ -1669,6 +1700,11 @@ spec:
|
||||
type: object
|
||||
insecureSkipVerify:
|
||||
type: boolean
|
||||
timeout:
|
||||
description: Timeout is the time to wait for a server's
|
||||
response. Its a duration e.g 15s, 2h30m. Missing value
|
||||
or empty string or means no timeout.
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
@@ -1684,6 +1720,11 @@ spec:
|
||||
type: object
|
||||
insecureSkipVerify:
|
||||
type: boolean
|
||||
timeout:
|
||||
description: Timeout is the time to wait for a server's
|
||||
response. Its a duration e.g 15s, 2h30m. Missing value
|
||||
or empty string or means no timeout.
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
@@ -1699,6 +1740,11 @@ spec:
|
||||
type: object
|
||||
insecureSkipVerify:
|
||||
type: boolean
|
||||
timeout:
|
||||
description: Timeout is the time to wait for a server's
|
||||
response. Its a duration e.g 15s, 2h30m. Missing value
|
||||
or empty string or means no timeout.
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
|
||||
@@ -1489,6 +1489,19 @@ spec:
|
||||
required:
|
||||
- outcomes
|
||||
type: object
|
||||
velero:
|
||||
properties:
|
||||
annotations:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
checkName:
|
||||
type: string
|
||||
exclude:
|
||||
type: BoolString
|
||||
strict:
|
||||
type: BoolString
|
||||
type: object
|
||||
weaveReport:
|
||||
properties:
|
||||
annotations:
|
||||
@@ -1815,6 +1828,17 @@ spec:
|
||||
- namespace
|
||||
- selector
|
||||
type: object
|
||||
helm:
|
||||
properties:
|
||||
collectorName:
|
||||
type: string
|
||||
exclude:
|
||||
type: BoolString
|
||||
namespace:
|
||||
type: string
|
||||
releaseName:
|
||||
type: string
|
||||
type: object
|
||||
http:
|
||||
properties:
|
||||
collectorName:
|
||||
@@ -1829,6 +1853,11 @@ spec:
|
||||
type: object
|
||||
insecureSkipVerify:
|
||||
type: boolean
|
||||
timeout:
|
||||
description: Timeout is the time to wait for a server's
|
||||
response. Its a duration e.g 15s, 2h30m. Missing value
|
||||
or empty string or means no timeout.
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
@@ -1846,6 +1875,11 @@ spec:
|
||||
type: object
|
||||
insecureSkipVerify:
|
||||
type: boolean
|
||||
timeout:
|
||||
description: Timeout is the time to wait for a server's
|
||||
response. Its a duration e.g 15s, 2h30m. Missing value
|
||||
or empty string or means no timeout.
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
@@ -1861,6 +1895,11 @@ spec:
|
||||
type: object
|
||||
insecureSkipVerify:
|
||||
type: boolean
|
||||
timeout:
|
||||
description: Timeout is the time to wait for a server's
|
||||
response. Its a duration e.g 15s, 2h30m. Missing value
|
||||
or empty string or means no timeout.
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
@@ -4097,6 +4136,31 @@ spec:
|
||||
info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/'
|
||||
type: object
|
||||
type: object
|
||||
restartPolicy:
|
||||
description: 'RestartPolicy defines the restart
|
||||
behavior of individual containers in a pod.
|
||||
This field may only be set for init containers,
|
||||
and the only allowed value is "Always". For
|
||||
non-init containers or when this field is not
|
||||
specified, the restart behavior is defined by
|
||||
the Pod''s restart policy and the container
|
||||
type. Setting the RestartPolicy as "Always"
|
||||
for the init container will have the following
|
||||
effect: this init container will be continually
|
||||
restarted on exit until all regular containers
|
||||
have terminated. Once all regular containers
|
||||
have completed, all init containers with restartPolicy
|
||||
"Always" will be shut down. This lifecycle differs
|
||||
from normal init containers and is often referred
|
||||
to as a "sidecar" container. Although this init
|
||||
container still starts in the init container
|
||||
sequence, it does not wait for the container
|
||||
to complete before proceeding to the next init
|
||||
container. Instead, the next init container
|
||||
starts immediately after this init container
|
||||
is started, or after any startupProbe has successfully
|
||||
completed.'
|
||||
type: string
|
||||
securityContext:
|
||||
description: 'SecurityContext defines the security
|
||||
options the container should be run with. If
|
||||
@@ -4236,8 +4300,8 @@ spec:
|
||||
preconfigured on the node to work. Must
|
||||
be a descending path, relative to the
|
||||
kubelet's configured seccomp profile
|
||||
location. Must only be set if type is
|
||||
"Localhost".
|
||||
location. Must be set if type is "Localhost".
|
||||
Must NOT be set for any other type.
|
||||
type: string
|
||||
type:
|
||||
description: "type indicates which kind
|
||||
@@ -4277,18 +4341,13 @@ spec:
|
||||
hostProcess:
|
||||
description: HostProcess determines if
|
||||
a container should be run as a 'Host
|
||||
Process' container. This field is alpha-level
|
||||
and will only be honored by components
|
||||
that enable the WindowsHostProcessContainers
|
||||
feature flag. Setting this field without
|
||||
the feature flag will result in errors
|
||||
when validating the Pod. All of a Pod's
|
||||
containers must have the same effective
|
||||
HostProcess value (it is not allowed
|
||||
to have a mix of HostProcess containers
|
||||
and non-HostProcess containers). In
|
||||
addition, if HostProcess is true then
|
||||
HostNetwork must also be set to true.
|
||||
Process' container. All of a Pod's containers
|
||||
must have the same effective HostProcess
|
||||
value (it is not allowed to have a mix
|
||||
of HostProcess containers and non-HostProcess
|
||||
containers). In addition, if HostProcess
|
||||
is true then HostNetwork must also be
|
||||
set to true.
|
||||
type: boolean
|
||||
runAsUserName:
|
||||
description: The UserName in Windows to
|
||||
@@ -5607,6 +5666,13 @@ spec:
|
||||
info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/'
|
||||
type: object
|
||||
type: object
|
||||
restartPolicy:
|
||||
description: Restart policy for the container
|
||||
to manage the restart behavior of each container
|
||||
within a pod. This may only be set for init
|
||||
containers. You cannot set this field on ephemeral
|
||||
containers.
|
||||
type: string
|
||||
securityContext:
|
||||
description: 'Optional: SecurityContext defines
|
||||
the security options the ephemeral container
|
||||
@@ -5745,8 +5811,8 @@ spec:
|
||||
preconfigured on the node to work. Must
|
||||
be a descending path, relative to the
|
||||
kubelet's configured seccomp profile
|
||||
location. Must only be set if type is
|
||||
"Localhost".
|
||||
location. Must be set if type is "Localhost".
|
||||
Must NOT be set for any other type.
|
||||
type: string
|
||||
type:
|
||||
description: "type indicates which kind
|
||||
@@ -5786,18 +5852,13 @@ spec:
|
||||
hostProcess:
|
||||
description: HostProcess determines if
|
||||
a container should be run as a 'Host
|
||||
Process' container. This field is alpha-level
|
||||
and will only be honored by components
|
||||
that enable the WindowsHostProcessContainers
|
||||
feature flag. Setting this field without
|
||||
the feature flag will result in errors
|
||||
when validating the Pod. All of a Pod's
|
||||
containers must have the same effective
|
||||
HostProcess value (it is not allowed
|
||||
to have a mix of HostProcess containers
|
||||
and non-HostProcess containers). In
|
||||
addition, if HostProcess is true then
|
||||
HostNetwork must also be set to true.
|
||||
Process' container. All of a Pod's containers
|
||||
must have the same effective HostProcess
|
||||
value (it is not allowed to have a mix
|
||||
of HostProcess containers and non-HostProcess
|
||||
containers). In addition, if HostProcess
|
||||
is true then HostNetwork must also be
|
||||
set to true.
|
||||
type: boolean
|
||||
runAsUserName:
|
||||
description: The UserName in Windows to
|
||||
@@ -7153,6 +7214,31 @@ spec:
|
||||
info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/'
|
||||
type: object
|
||||
type: object
|
||||
restartPolicy:
|
||||
description: 'RestartPolicy defines the restart
|
||||
behavior of individual containers in a pod.
|
||||
This field may only be set for init containers,
|
||||
and the only allowed value is "Always". For
|
||||
non-init containers or when this field is not
|
||||
specified, the restart behavior is defined by
|
||||
the Pod''s restart policy and the container
|
||||
type. Setting the RestartPolicy as "Always"
|
||||
for the init container will have the following
|
||||
effect: this init container will be continually
|
||||
restarted on exit until all regular containers
|
||||
have terminated. Once all regular containers
|
||||
have completed, all init containers with restartPolicy
|
||||
"Always" will be shut down. This lifecycle differs
|
||||
from normal init containers and is often referred
|
||||
to as a "sidecar" container. Although this init
|
||||
container still starts in the init container
|
||||
sequence, it does not wait for the container
|
||||
to complete before proceeding to the next init
|
||||
container. Instead, the next init container
|
||||
starts immediately after this init container
|
||||
is started, or after any startupProbe has successfully
|
||||
completed.'
|
||||
type: string
|
||||
securityContext:
|
||||
description: 'SecurityContext defines the security
|
||||
options the container should be run with. If
|
||||
@@ -7292,8 +7378,8 @@ spec:
|
||||
preconfigured on the node to work. Must
|
||||
be a descending path, relative to the
|
||||
kubelet's configured seccomp profile
|
||||
location. Must only be set if type is
|
||||
"Localhost".
|
||||
location. Must be set if type is "Localhost".
|
||||
Must NOT be set for any other type.
|
||||
type: string
|
||||
type:
|
||||
description: "type indicates which kind
|
||||
@@ -7333,18 +7419,13 @@ spec:
|
||||
hostProcess:
|
||||
description: HostProcess determines if
|
||||
a container should be run as a 'Host
|
||||
Process' container. This field is alpha-level
|
||||
and will only be honored by components
|
||||
that enable the WindowsHostProcessContainers
|
||||
feature flag. Setting this field without
|
||||
the feature flag will result in errors
|
||||
when validating the Pod. All of a Pod's
|
||||
containers must have the same effective
|
||||
HostProcess value (it is not allowed
|
||||
to have a mix of HostProcess containers
|
||||
and non-HostProcess containers). In
|
||||
addition, if HostProcess is true then
|
||||
HostNetwork must also be set to true.
|
||||
Process' container. All of a Pod's containers
|
||||
must have the same effective HostProcess
|
||||
value (it is not allowed to have a mix
|
||||
of HostProcess containers and non-HostProcess
|
||||
containers). In addition, if HostProcess
|
||||
is true then HostNetwork must also be
|
||||
set to true.
|
||||
type: boolean
|
||||
runAsUserName:
|
||||
description: The UserName in Windows to
|
||||
@@ -7806,17 +7887,10 @@ spec:
|
||||
template will be used to create a new ResourceClaim,
|
||||
which will be bound to this pod. When this
|
||||
pod is deleted, the ResourceClaim will also
|
||||
be deleted. The name of the ResourceClaim
|
||||
will be <pod name>-<resource name>, where
|
||||
<resource name> is the PodResourceClaim.Name.
|
||||
Pod validation will reject the pod if the
|
||||
concatenated name is not valid for a ResourceClaim
|
||||
(e.g. too long). \n An existing ResourceClaim
|
||||
with that name that is not owned by the
|
||||
pod will not be used for the pod to avoid
|
||||
using an unrelated resource by mistake.
|
||||
Scheduling and pod startup are then blocked
|
||||
until the unrelated ResourceClaim is removed.
|
||||
be deleted. The pod name and resource name,
|
||||
along with a generated component, will be
|
||||
used to form a unique name for the ResourceClaim,
|
||||
which will be recorded in pod.status.resourceClaimStatuses.
|
||||
\n This field is immutable and no changes
|
||||
will be made to the corresponding ResourceClaim
|
||||
by the control plane after creating the
|
||||
@@ -7977,7 +8051,8 @@ spec:
|
||||
The profile must be preconfigured on the node
|
||||
to work. Must be a descending path, relative
|
||||
to the kubelet's configured seccomp profile
|
||||
location. Must only be set if type is "Localhost".
|
||||
location. Must be set if type is "Localhost".
|
||||
Must NOT be set for any other type.
|
||||
type: string
|
||||
type:
|
||||
description: "type indicates which kind of seccomp
|
||||
@@ -8050,14 +8125,10 @@ spec:
|
||||
hostProcess:
|
||||
description: HostProcess determines if a container
|
||||
should be run as a 'Host Process' container.
|
||||
This field is alpha-level and will only be
|
||||
honored by components that enable the WindowsHostProcessContainers
|
||||
feature flag. Setting this field without the
|
||||
feature flag will result in errors when validating
|
||||
the Pod. All of a Pod's containers must have
|
||||
the same effective HostProcess value (it is
|
||||
not allowed to have a mix of HostProcess containers
|
||||
and non-HostProcess containers). In addition,
|
||||
All of a Pod's containers must have the same
|
||||
effective HostProcess value (it is not allowed
|
||||
to have a mix of HostProcess containers and
|
||||
non-HostProcess containers). In addition,
|
||||
if HostProcess is true then HostNetwork must
|
||||
also be set to true.
|
||||
type: boolean
|
||||
@@ -10405,6 +10476,11 @@ spec:
|
||||
type: object
|
||||
insecureSkipVerify:
|
||||
type: boolean
|
||||
timeout:
|
||||
description: Timeout is the time to wait for a server's
|
||||
response. Its a duration e.g 15s, 2h30m. Missing value
|
||||
or empty string or means no timeout.
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
@@ -10420,6 +10496,11 @@ spec:
|
||||
type: object
|
||||
insecureSkipVerify:
|
||||
type: boolean
|
||||
timeout:
|
||||
description: Timeout is the time to wait for a server's
|
||||
response. Its a duration e.g 15s, 2h30m. Missing value
|
||||
or empty string or means no timeout.
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
@@ -10435,6 +10516,11 @@ spec:
|
||||
type: object
|
||||
insecureSkipVerify:
|
||||
type: boolean
|
||||
timeout:
|
||||
description: Timeout is the time to wait for a server's
|
||||
response. Its a duration e.g 15s, 2h30m. Missing value
|
||||
or empty string or means no timeout.
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
|
||||
@@ -207,6 +207,11 @@ spec:
|
||||
type: object
|
||||
insecureSkipVerify:
|
||||
type: boolean
|
||||
timeout:
|
||||
description: Timeout is the time to wait for a server's
|
||||
response. Its a duration e.g 15s, 2h30m. Missing value
|
||||
or empty string or means no timeout.
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
@@ -222,6 +227,11 @@ spec:
|
||||
type: object
|
||||
insecureSkipVerify:
|
||||
type: boolean
|
||||
timeout:
|
||||
description: Timeout is the time to wait for a server's
|
||||
response. Its a duration e.g 15s, 2h30m. Missing value
|
||||
or empty string or means no timeout.
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
@@ -237,6 +247,11 @@ spec:
|
||||
type: object
|
||||
insecureSkipVerify:
|
||||
type: boolean
|
||||
timeout:
|
||||
description: Timeout is the time to wait for a server's
|
||||
response. Its a duration e.g 15s, 2h30m. Missing value
|
||||
or empty string or means no timeout.
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
|
||||
@@ -1520,6 +1520,19 @@ spec:
|
||||
required:
|
||||
- outcomes
|
||||
type: object
|
||||
velero:
|
||||
properties:
|
||||
annotations:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
checkName:
|
||||
type: string
|
||||
exclude:
|
||||
type: BoolString
|
||||
strict:
|
||||
type: BoolString
|
||||
type: object
|
||||
weaveReport:
|
||||
properties:
|
||||
annotations:
|
||||
@@ -1846,6 +1859,17 @@ spec:
|
||||
- namespace
|
||||
- selector
|
||||
type: object
|
||||
helm:
|
||||
properties:
|
||||
collectorName:
|
||||
type: string
|
||||
exclude:
|
||||
type: BoolString
|
||||
namespace:
|
||||
type: string
|
||||
releaseName:
|
||||
type: string
|
||||
type: object
|
||||
http:
|
||||
properties:
|
||||
collectorName:
|
||||
@@ -1860,6 +1884,11 @@ spec:
|
||||
type: object
|
||||
insecureSkipVerify:
|
||||
type: boolean
|
||||
timeout:
|
||||
description: Timeout is the time to wait for a server's
|
||||
response. Its a duration e.g 15s, 2h30m. Missing value
|
||||
or empty string or means no timeout.
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
@@ -1877,6 +1906,11 @@ spec:
|
||||
type: object
|
||||
insecureSkipVerify:
|
||||
type: boolean
|
||||
timeout:
|
||||
description: Timeout is the time to wait for a server's
|
||||
response. Its a duration e.g 15s, 2h30m. Missing value
|
||||
or empty string or means no timeout.
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
@@ -1892,6 +1926,11 @@ spec:
|
||||
type: object
|
||||
insecureSkipVerify:
|
||||
type: boolean
|
||||
timeout:
|
||||
description: Timeout is the time to wait for a server's
|
||||
response. Its a duration e.g 15s, 2h30m. Missing value
|
||||
or empty string or means no timeout.
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
@@ -4128,6 +4167,31 @@ spec:
|
||||
info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/'
|
||||
type: object
|
||||
type: object
|
||||
restartPolicy:
|
||||
description: 'RestartPolicy defines the restart
|
||||
behavior of individual containers in a pod.
|
||||
This field may only be set for init containers,
|
||||
and the only allowed value is "Always". For
|
||||
non-init containers or when this field is not
|
||||
specified, the restart behavior is defined by
|
||||
the Pod''s restart policy and the container
|
||||
type. Setting the RestartPolicy as "Always"
|
||||
for the init container will have the following
|
||||
effect: this init container will be continually
|
||||
restarted on exit until all regular containers
|
||||
have terminated. Once all regular containers
|
||||
have completed, all init containers with restartPolicy
|
||||
"Always" will be shut down. This lifecycle differs
|
||||
from normal init containers and is often referred
|
||||
to as a "sidecar" container. Although this init
|
||||
container still starts in the init container
|
||||
sequence, it does not wait for the container
|
||||
to complete before proceeding to the next init
|
||||
container. Instead, the next init container
|
||||
starts immediately after this init container
|
||||
is started, or after any startupProbe has successfully
|
||||
completed.'
|
||||
type: string
|
||||
securityContext:
|
||||
description: 'SecurityContext defines the security
|
||||
options the container should be run with. If
|
||||
@@ -4267,8 +4331,8 @@ spec:
|
||||
preconfigured on the node to work. Must
|
||||
be a descending path, relative to the
|
||||
kubelet's configured seccomp profile
|
||||
location. Must only be set if type is
|
||||
"Localhost".
|
||||
location. Must be set if type is "Localhost".
|
||||
Must NOT be set for any other type.
|
||||
type: string
|
||||
type:
|
||||
description: "type indicates which kind
|
||||
@@ -4308,18 +4372,13 @@ spec:
|
||||
hostProcess:
|
||||
description: HostProcess determines if
|
||||
a container should be run as a 'Host
|
||||
Process' container. This field is alpha-level
|
||||
and will only be honored by components
|
||||
that enable the WindowsHostProcessContainers
|
||||
feature flag. Setting this field without
|
||||
the feature flag will result in errors
|
||||
when validating the Pod. All of a Pod's
|
||||
containers must have the same effective
|
||||
HostProcess value (it is not allowed
|
||||
to have a mix of HostProcess containers
|
||||
and non-HostProcess containers). In
|
||||
addition, if HostProcess is true then
|
||||
HostNetwork must also be set to true.
|
||||
Process' container. All of a Pod's containers
|
||||
must have the same effective HostProcess
|
||||
value (it is not allowed to have a mix
|
||||
of HostProcess containers and non-HostProcess
|
||||
containers). In addition, if HostProcess
|
||||
is true then HostNetwork must also be
|
||||
set to true.
|
||||
type: boolean
|
||||
runAsUserName:
|
||||
description: The UserName in Windows to
|
||||
@@ -5638,6 +5697,13 @@ spec:
|
||||
info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/'
|
||||
type: object
|
||||
type: object
|
||||
restartPolicy:
|
||||
description: Restart policy for the container
|
||||
to manage the restart behavior of each container
|
||||
within a pod. This may only be set for init
|
||||
containers. You cannot set this field on ephemeral
|
||||
containers.
|
||||
type: string
|
||||
securityContext:
|
||||
description: 'Optional: SecurityContext defines
|
||||
the security options the ephemeral container
|
||||
@@ -5776,8 +5842,8 @@ spec:
|
||||
preconfigured on the node to work. Must
|
||||
be a descending path, relative to the
|
||||
kubelet's configured seccomp profile
|
||||
location. Must only be set if type is
|
||||
"Localhost".
|
||||
location. Must be set if type is "Localhost".
|
||||
Must NOT be set for any other type.
|
||||
type: string
|
||||
type:
|
||||
description: "type indicates which kind
|
||||
@@ -5817,18 +5883,13 @@ spec:
|
||||
hostProcess:
|
||||
description: HostProcess determines if
|
||||
a container should be run as a 'Host
|
||||
Process' container. This field is alpha-level
|
||||
and will only be honored by components
|
||||
that enable the WindowsHostProcessContainers
|
||||
feature flag. Setting this field without
|
||||
the feature flag will result in errors
|
||||
when validating the Pod. All of a Pod's
|
||||
containers must have the same effective
|
||||
HostProcess value (it is not allowed
|
||||
to have a mix of HostProcess containers
|
||||
and non-HostProcess containers). In
|
||||
addition, if HostProcess is true then
|
||||
HostNetwork must also be set to true.
|
||||
Process' container. All of a Pod's containers
|
||||
must have the same effective HostProcess
|
||||
value (it is not allowed to have a mix
|
||||
of HostProcess containers and non-HostProcess
|
||||
containers). In addition, if HostProcess
|
||||
is true then HostNetwork must also be
|
||||
set to true.
|
||||
type: boolean
|
||||
runAsUserName:
|
||||
description: The UserName in Windows to
|
||||
@@ -7184,6 +7245,31 @@ spec:
|
||||
info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/'
|
||||
type: object
|
||||
type: object
|
||||
restartPolicy:
|
||||
description: 'RestartPolicy defines the restart
|
||||
behavior of individual containers in a pod.
|
||||
This field may only be set for init containers,
|
||||
and the only allowed value is "Always". For
|
||||
non-init containers or when this field is not
|
||||
specified, the restart behavior is defined by
|
||||
the Pod''s restart policy and the container
|
||||
type. Setting the RestartPolicy as "Always"
|
||||
for the init container will have the following
|
||||
effect: this init container will be continually
|
||||
restarted on exit until all regular containers
|
||||
have terminated. Once all regular containers
|
||||
have completed, all init containers with restartPolicy
|
||||
"Always" will be shut down. This lifecycle differs
|
||||
from normal init containers and is often referred
|
||||
to as a "sidecar" container. Although this init
|
||||
container still starts in the init container
|
||||
sequence, it does not wait for the container
|
||||
to complete before proceeding to the next init
|
||||
container. Instead, the next init container
|
||||
starts immediately after this init container
|
||||
is started, or after any startupProbe has successfully
|
||||
completed.'
|
||||
type: string
|
||||
securityContext:
|
||||
description: 'SecurityContext defines the security
|
||||
options the container should be run with. If
|
||||
@@ -7323,8 +7409,8 @@ spec:
|
||||
preconfigured on the node to work. Must
|
||||
be a descending path, relative to the
|
||||
kubelet's configured seccomp profile
|
||||
location. Must only be set if type is
|
||||
"Localhost".
|
||||
location. Must be set if type is "Localhost".
|
||||
Must NOT be set for any other type.
|
||||
type: string
|
||||
type:
|
||||
description: "type indicates which kind
|
||||
@@ -7364,18 +7450,13 @@ spec:
|
||||
hostProcess:
|
||||
description: HostProcess determines if
|
||||
a container should be run as a 'Host
|
||||
Process' container. This field is alpha-level
|
||||
and will only be honored by components
|
||||
that enable the WindowsHostProcessContainers
|
||||
feature flag. Setting this field without
|
||||
the feature flag will result in errors
|
||||
when validating the Pod. All of a Pod's
|
||||
containers must have the same effective
|
||||
HostProcess value (it is not allowed
|
||||
to have a mix of HostProcess containers
|
||||
and non-HostProcess containers). In
|
||||
addition, if HostProcess is true then
|
||||
HostNetwork must also be set to true.
|
||||
Process' container. All of a Pod's containers
|
||||
must have the same effective HostProcess
|
||||
value (it is not allowed to have a mix
|
||||
of HostProcess containers and non-HostProcess
|
||||
containers). In addition, if HostProcess
|
||||
is true then HostNetwork must also be
|
||||
set to true.
|
||||
type: boolean
|
||||
runAsUserName:
|
||||
description: The UserName in Windows to
|
||||
@@ -7837,17 +7918,10 @@ spec:
|
||||
template will be used to create a new ResourceClaim,
|
||||
which will be bound to this pod. When this
|
||||
pod is deleted, the ResourceClaim will also
|
||||
be deleted. The name of the ResourceClaim
|
||||
will be <pod name>-<resource name>, where
|
||||
<resource name> is the PodResourceClaim.Name.
|
||||
Pod validation will reject the pod if the
|
||||
concatenated name is not valid for a ResourceClaim
|
||||
(e.g. too long). \n An existing ResourceClaim
|
||||
with that name that is not owned by the
|
||||
pod will not be used for the pod to avoid
|
||||
using an unrelated resource by mistake.
|
||||
Scheduling and pod startup are then blocked
|
||||
until the unrelated ResourceClaim is removed.
|
||||
be deleted. The pod name and resource name,
|
||||
along with a generated component, will be
|
||||
used to form a unique name for the ResourceClaim,
|
||||
which will be recorded in pod.status.resourceClaimStatuses.
|
||||
\n This field is immutable and no changes
|
||||
will be made to the corresponding ResourceClaim
|
||||
by the control plane after creating the
|
||||
@@ -8008,7 +8082,8 @@ spec:
|
||||
The profile must be preconfigured on the node
|
||||
to work. Must be a descending path, relative
|
||||
to the kubelet's configured seccomp profile
|
||||
location. Must only be set if type is "Localhost".
|
||||
location. Must be set if type is "Localhost".
|
||||
Must NOT be set for any other type.
|
||||
type: string
|
||||
type:
|
||||
description: "type indicates which kind of seccomp
|
||||
@@ -8081,14 +8156,10 @@ spec:
|
||||
hostProcess:
|
||||
description: HostProcess determines if a container
|
||||
should be run as a 'Host Process' container.
|
||||
This field is alpha-level and will only be
|
||||
honored by components that enable the WindowsHostProcessContainers
|
||||
feature flag. Setting this field without the
|
||||
feature flag will result in errors when validating
|
||||
the Pod. All of a Pod's containers must have
|
||||
the same effective HostProcess value (it is
|
||||
not allowed to have a mix of HostProcess containers
|
||||
and non-HostProcess containers). In addition,
|
||||
All of a Pod's containers must have the same
|
||||
effective HostProcess value (it is not allowed
|
||||
to have a mix of HostProcess containers and
|
||||
non-HostProcess containers). In addition,
|
||||
if HostProcess is true then HostNetwork must
|
||||
also be set to true.
|
||||
type: boolean
|
||||
@@ -11520,6 +11591,11 @@ spec:
|
||||
type: object
|
||||
insecureSkipVerify:
|
||||
type: boolean
|
||||
timeout:
|
||||
description: Timeout is the time to wait for a server's
|
||||
response. Its a duration e.g 15s, 2h30m. Missing value
|
||||
or empty string or means no timeout.
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
@@ -11535,6 +11611,11 @@ spec:
|
||||
type: object
|
||||
insecureSkipVerify:
|
||||
type: boolean
|
||||
timeout:
|
||||
description: Timeout is the time to wait for a server's
|
||||
response. Its a duration e.g 15s, 2h30m. Missing value
|
||||
or empty string or means no timeout.
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
@@ -11550,6 +11631,11 @@ spec:
|
||||
type: object
|
||||
insecureSkipVerify:
|
||||
type: boolean
|
||||
timeout:
|
||||
description: Timeout is the time to wait for a server's
|
||||
response. Its a duration e.g 15s, 2h30m. Missing value
|
||||
or empty string or means no timeout.
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
@@ -11613,8 +11699,24 @@ spec:
|
||||
type: string
|
||||
command:
|
||||
type: string
|
||||
env:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
exclude:
|
||||
type: BoolString
|
||||
ignoreParentEnvs:
|
||||
type: boolean
|
||||
inheritEnvs:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
input:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
outputDir:
|
||||
type: string
|
||||
required:
|
||||
- args
|
||||
- command
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
project_name: troubleshoot
|
||||
release:
|
||||
github:
|
||||
owner: replicatedhq
|
||||
name: troubleshoot
|
||||
builds:
|
||||
- id: preflight
|
||||
# NOTE: if you add any additional goos/goarch values, ensure you update ../.github/workflows/build-test-deploy.yaml
|
||||
@@ -15,6 +11,7 @@ builds:
|
||||
- amd64
|
||||
- arm
|
||||
- arm64
|
||||
- riscv64
|
||||
ignore:
|
||||
- goos: windows
|
||||
goarch: arm
|
||||
@@ -38,6 +35,7 @@ builds:
|
||||
- amd64
|
||||
- arm
|
||||
- arm64
|
||||
- riscv64
|
||||
ignore:
|
||||
- goos: windows
|
||||
goarch: arm
|
||||
@@ -61,6 +59,7 @@ builds:
|
||||
- amd64
|
||||
- arm
|
||||
- arm64
|
||||
- riscv64
|
||||
ignore:
|
||||
- goos: windows
|
||||
goarch: arm
|
||||
|
||||
+2
-1
@@ -29,6 +29,7 @@ preflight [url] [flags]
|
||||
--cpuprofile string File path to write cpu profiling data
|
||||
--debug enable debug logging
|
||||
--disable-compression If true, opt-out of response compression for all requests to the server
|
||||
--dry-run print the preflight spec without running preflight checks
|
||||
--format string output format, one of human, json, yaml. only used when interactive is set to false (default "human")
|
||||
-h, --help help for preflight
|
||||
--insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure
|
||||
@@ -53,4 +54,4 @@ preflight [url] [flags]
|
||||
* [preflight oci-fetch](preflight_oci-fetch.md) - Fetch a preflight from an OCI registry and print it to standard out
|
||||
* [preflight version](preflight_version.md) - Print the current version and exit
|
||||
|
||||
###### Auto generated by spf13/cobra on 8-Jun-2023
|
||||
###### Auto generated by spf13/cobra on 31-Aug-2023
|
||||
|
||||
@@ -33,4 +33,4 @@ preflight oci-fetch [URI] [flags]
|
||||
|
||||
* [preflight](preflight.md) - Run and retrieve preflight checks in a cluster
|
||||
|
||||
###### Auto generated by spf13/cobra on 8-Jun-2023
|
||||
###### Auto generated by spf13/cobra on 31-Aug-2023
|
||||
|
||||
@@ -37,4 +37,4 @@ preflight version [flags]
|
||||
|
||||
* [preflight](preflight.md) - Run and retrieve preflight checks in a cluster
|
||||
|
||||
###### Auto generated by spf13/cobra on 3-Jan-2023
|
||||
###### Auto generated by spf13/cobra on 31-Aug-2023
|
||||
|
||||
@@ -55,4 +55,4 @@ support-bundle [urls...] [flags]
|
||||
* [support-bundle redact](support-bundle_redact.md) - Redact information from a generated support bundle archive
|
||||
* [support-bundle version](support-bundle_version.md) - Print the current version and exit
|
||||
|
||||
###### Auto generated by spf13/cobra on 8-Jun-2023
|
||||
###### Auto generated by spf13/cobra on 31-Aug-2023
|
||||
|
||||
@@ -30,4 +30,4 @@ support-bundle analyze [url] [flags]
|
||||
|
||||
* [support-bundle](support-bundle.md) - Generate a support bundle
|
||||
|
||||
###### Auto generated by spf13/cobra on 3-Jan-2023
|
||||
###### Auto generated by spf13/cobra on 31-Aug-2023
|
||||
|
||||
@@ -39,4 +39,4 @@ support-bundle redact [urls...] [flags]
|
||||
|
||||
* [support-bundle](support-bundle.md) - Generate a support bundle
|
||||
|
||||
###### Auto generated by spf13/cobra on 3-Jan-2023
|
||||
###### Auto generated by spf13/cobra on 31-Aug-2023
|
||||
|
||||
@@ -27,4 +27,4 @@ support-bundle version [flags]
|
||||
|
||||
* [support-bundle](support-bundle.md) - Generate a support bundle
|
||||
|
||||
###### Auto generated by spf13/cobra on 3-Jan-2023
|
||||
###### Auto generated by spf13/cobra on 31-Aug-2023
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
apiVersion: troubleshoot.sh/v1beta2
|
||||
kind: HostCollector
|
||||
metadata:
|
||||
name: run-host-cmd-and-save-output
|
||||
spec:
|
||||
collectors:
|
||||
- run:
|
||||
collectorName: "my-custom-run"
|
||||
command: "sh"
|
||||
# this is for demonstration purpose only -- you probably don't want to drop your input to the bundle!
|
||||
args:
|
||||
- "-c"
|
||||
- "cat $TS_INPUT_DIR/dummy.yaml > $TS_OUTPUT_DIR/dummy_content.yaml"
|
||||
outputDir: "myCommandOutputs"
|
||||
env:
|
||||
- AWS_REGION=us-west-1
|
||||
# if ignoreParentEnvs is true, it will not inherit envs from parent process.
|
||||
# values specified in inheritEnv will not be used either
|
||||
# ignoreParentEnvs: true
|
||||
inheritEnvs:
|
||||
- USER
|
||||
input:
|
||||
dummy.conf: |-
|
||||
[hello]
|
||||
hello = 1
|
||||
|
||||
[bye]
|
||||
bye = 2
|
||||
dummy.yaml: |-
|
||||
username: postgres
|
||||
password: <my-pass>
|
||||
dbHost: <hostname>
|
||||
map:
|
||||
key: value
|
||||
list:
|
||||
- val1
|
||||
- val2
|
||||
@@ -19,4 +19,3 @@ spec:
|
||||
message: At least 16 CPU cores preferred
|
||||
- pass:
|
||||
message: This server has sufficient CPU cores
|
||||
|
||||
|
||||
@@ -72,6 +72,9 @@ spec:
|
||||
- pass:
|
||||
when: "== k3s"
|
||||
message: K3S is a supported distribution
|
||||
- pass:
|
||||
when: "== oke"
|
||||
message: OKE is a supported distribution
|
||||
- warn:
|
||||
message: Unable to determine the distribution of Kubernetes
|
||||
- nodeResources:
|
||||
|
||||
@@ -9,7 +9,7 @@ replace github.com/replicatedhq/troubleshoot v0.0.0 => ../../../
|
||||
|
||||
require (
|
||||
github.com/replicatedhq/troubleshoot v0.0.0
|
||||
helm.sh/helm/v3 v3.12.1
|
||||
helm.sh/helm/v3 v3.12.3
|
||||
sigs.k8s.io/yaml v1.3.0
|
||||
)
|
||||
|
||||
@@ -18,8 +18,8 @@ require (
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.2.1 // indirect
|
||||
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.2.3 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.10.2 // indirect
|
||||
github.com/go-logr/logr v1.2.4 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.6 // indirect
|
||||
@@ -28,10 +28,10 @@ require (
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/google/gnostic v0.6.9 // indirect
|
||||
github.com/google/gnostic-models v0.6.8 // indirect
|
||||
github.com/google/go-cmp v0.5.9 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/google/uuid v1.3.1 // indirect
|
||||
github.com/huandu/xstrings v1.4.0 // indirect
|
||||
github.com/imdario/mergo v0.3.15 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
@@ -48,26 +48,26 @@ require (
|
||||
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
|
||||
golang.org/x/crypto v0.9.0 // indirect
|
||||
golang.org/x/net v0.10.0 // indirect
|
||||
golang.org/x/oauth2 v0.7.0 // indirect
|
||||
golang.org/x/sys v0.8.0 // indirect
|
||||
golang.org/x/term v0.8.0 // indirect
|
||||
golang.org/x/text v0.10.0 // indirect
|
||||
golang.org/x/crypto v0.14.0 // indirect
|
||||
golang.org/x/net v0.17.0 // indirect
|
||||
golang.org/x/oauth2 v0.12.0 // indirect
|
||||
golang.org/x/sys v0.13.0 // indirect
|
||||
golang.org/x/term v0.13.0 // indirect
|
||||
golang.org/x/text v0.13.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/protobuf v1.30.0 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/api v0.27.3 // indirect
|
||||
k8s.io/apiextensions-apiserver v0.27.2 // indirect
|
||||
k8s.io/apimachinery v0.27.3 // indirect
|
||||
k8s.io/client-go v0.27.3 // indirect
|
||||
k8s.io/api v0.28.2 // indirect
|
||||
k8s.io/apiextensions-apiserver v0.28.1 // indirect
|
||||
k8s.io/apimachinery v0.28.2 // indirect
|
||||
k8s.io/client-go v0.28.2 // indirect
|
||||
k8s.io/klog/v2 v2.100.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect
|
||||
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect
|
||||
sigs.k8s.io/controller-runtime v0.15.0 // indirect
|
||||
sigs.k8s.io/controller-runtime v0.16.2 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
|
||||
)
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
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=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
|
||||
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
||||
@@ -10,33 +7,16 @@ github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0
|
||||
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||
github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
|
||||
github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
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-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/cyphar/filepath-securejoin v0.2.3 h1:YX6ebbZCZP7VkM3scTTokDgBL2TY741X51MTk3ycuNI=
|
||||
github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
|
||||
github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg=
|
||||
github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
||||
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/emicklei/go-restful/v3 v3.10.2 h1:hIovbnmBTLjHXkqEBUz3HGpXZdM7ZrE9fJIZIqlJLqE=
|
||||
github.com/emicklei/go-restful/v3 v3.10.2/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.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/flowstack/go-jsonschema v0.1.1/go.mod h1:yL7fNggx1o8rm9RlgXv7hTBWxdBM0rVwpMwimd3F3N0=
|
||||
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
|
||||
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
@@ -51,31 +31,12 @@ 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/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
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.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.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/gnostic v0.6.9 h1:ZK/5VhkoX835RikCHpSUJV9a+S3e1zLh59YnyWeBW+0=
|
||||
github.com/google/gnostic v0.6.9/go.mod h1:Nm8234We1lq6iB9OmlgNv3nH91XLLVZHCDayfA3xq+E=
|
||||
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.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
|
||||
github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
@@ -84,10 +45,8 @@ 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/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
|
||||
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
|
||||
github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
@@ -100,7 +59,6 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
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/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
@@ -122,24 +80,21 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
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/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
|
||||
github.com/onsi/gomega v1.27.7 h1:fVih9JD6ogIiHUN6ePK7HJidyEDpWGVB5mzM7cWNXoU=
|
||||
github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA=
|
||||
github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU=
|
||||
github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
|
||||
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=
|
||||
github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
@@ -160,161 +115,103 @@ github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQ
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
|
||||
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
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-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
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.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
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-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-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/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-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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g=
|
||||
golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=
|
||||
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/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4=
|
||||
golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4=
|
||||
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-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/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-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-20210510120138-977fb7262007/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-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
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.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
|
||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||
golang.org/x/text v0.3.0/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.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.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
|
||||
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
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-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-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo=
|
||||
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
|
||||
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=
|
||||
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.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||
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-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
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.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
|
||||
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
|
||||
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.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.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
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.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/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-20200615113413-eeeca48fe776/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=
|
||||
helm.sh/helm/v3 v3.12.1 h1:lzU7etZX24A6BTMXYQF3bFq0ECfD8s+fKlNBBL8AbEc=
|
||||
helm.sh/helm/v3 v3.12.1/go.mod h1:qhmSY9kcX7yH1xebe+FDMZa7E5NAeZ+LvK5j1gSln48=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
k8s.io/api v0.27.3 h1:yR6oQXXnUEBWEWcvPWS0jQL575KoAboQPfJAuKNrw5Y=
|
||||
k8s.io/api v0.27.3/go.mod h1:C4BNvZnQOF7JA/0Xed2S+aUyJSfTGkGFxLXz9MnpIpg=
|
||||
k8s.io/apiextensions-apiserver v0.27.2 h1:iwhyoeS4xj9Y7v8YExhUwbVuBhMr3Q4bd/laClBV6Bo=
|
||||
k8s.io/apiextensions-apiserver v0.27.2/go.mod h1:Oz9UdvGguL3ULgRdY9QMUzL2RZImotgxvGjdWRq6ZXQ=
|
||||
k8s.io/apimachinery v0.27.3 h1:Ubye8oBufD04l9QnNtW05idcOe9Z3GQN8+7PqmuVcUM=
|
||||
k8s.io/apimachinery v0.27.3/go.mod h1:XNfZ6xklnMCOGGFNqXG7bUrQCoR04dh/E7FprV6pb+E=
|
||||
k8s.io/client-go v0.27.3 h1:7dnEGHZEJld3lYwxvLl7WoehK6lAq7GvgjxpA3nv1E8=
|
||||
k8s.io/client-go v0.27.3/go.mod h1:2MBEKuTo6V1lbKy3z1euEGnhPfGZLKTS9tiJ2xodM48=
|
||||
helm.sh/helm/v3 v3.12.3 h1:5y1+Sbty12t48T/t/CGNYUIME5BJ0WKfmW/sobYqkFg=
|
||||
helm.sh/helm/v3 v3.12.3/go.mod h1:KPKQiX9IP5HX7o5YnnhViMnNuKiL/lJBVQ47GHe1R0k=
|
||||
k8s.io/api v0.28.2 h1:9mpl5mOb6vXZvqbQmankOfPIGiudghwCoLl1EYfUZbw=
|
||||
k8s.io/api v0.28.2/go.mod h1:RVnJBsjU8tcMq7C3iaRSGMeaKt2TWEUXcpIt/90fjEg=
|
||||
k8s.io/apiextensions-apiserver v0.28.1 h1:l2ThkBRjrWpw4f24uq0Da2HaEgqJZ7pcgiEUTKSmQZw=
|
||||
k8s.io/apiextensions-apiserver v0.28.1/go.mod h1:sVvrI+P4vxh2YBBcm8n2ThjNyzU4BQGilCQ/JAY5kGs=
|
||||
k8s.io/apimachinery v0.28.2 h1:KCOJLrc6gu+wV1BYgwik4AF4vXOlVJPdiqn0yAWWwXQ=
|
||||
k8s.io/apimachinery v0.28.2/go.mod h1:RdzF87y/ngqk9H4z3EL2Rppv5jj95vGS/HaFXrLDApU=
|
||||
k8s.io/client-go v0.28.2 h1:DNoYI1vGq0slMBN/SWKMZMw0Rq+0EQW6/AK4v9+3VeY=
|
||||
k8s.io/client-go v0.28.2/go.mod h1:sMkApowspLuc7omj1FOSUxSoqjr+d5Q0Yc0LOFnYFJY=
|
||||
k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg=
|
||||
k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
|
||||
k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f h1:2kWPakN3i/k81b0gvD5C5FJ2kxm1WrQFanWchyKuqGg=
|
||||
k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f/go.mod h1:byini6yhqGC14c3ebc/QwanvYwhuMWF6yz2F8uwW8eg=
|
||||
k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 h1:LyMgNKD2P8Wn1iAwQU5OhxCKlKJy0sHc+PcDwFB24dQ=
|
||||
k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9/go.mod h1:wZK2AVp1uHCp4VamDVgBP2COHZjqD1T68Rf0CM3YjSM=
|
||||
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 h1:qY1Ad8PODbnymg2pRbkyMT/ylpTrCM8P2RJ0yroCyIk=
|
||||
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
sigs.k8s.io/controller-runtime v0.15.0 h1:ML+5Adt3qZnMSYxZ7gAverBLNPSMQEibtzAgp0UPojU=
|
||||
sigs.k8s.io/controller-runtime v0.15.0/go.mod h1:7ngYvp1MLT+9GeZ+6lH3LOlcHkp/+tzA/fmHa4iq9kk=
|
||||
sigs.k8s.io/controller-runtime v0.16.2 h1:mwXAVuEk3EQf478PQwQ48zGOXvW27UJc8NHktQVuIPU=
|
||||
sigs.k8s.io/controller-runtime v0.16.2/go.mod h1:vpMu3LpI5sYWtujJOa2uPK61nB5rbwlN7BAB8aSLvGU=
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE=
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module github.com/replicatedhq/troubleshoot
|
||||
|
||||
go 1.19
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/ahmetalpbalkan/go-cursor v0.0.0-20131010032410-8136607ea412
|
||||
@@ -24,76 +24,106 @@ require (
|
||||
github.com/manifoldco/promptui v0.9.0
|
||||
github.com/mattn/go-isatty v0.0.19
|
||||
github.com/mholt/archiver/v3 v3.5.1
|
||||
github.com/microsoft/go-mssqldb v1.5.0
|
||||
github.com/microsoft/go-mssqldb v1.6.0
|
||||
github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/replicatedhq/termui/v3 v3.1.1-0.20200811145416-f40076d26851
|
||||
github.com/segmentio/ksuid v1.0.4
|
||||
github.com/shirou/gopsutil/v3 v3.23.7
|
||||
github.com/shirou/gopsutil/v3 v3.23.9
|
||||
github.com/spf13/cobra v1.7.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/spf13/viper v1.16.0
|
||||
github.com/spf13/viper v1.17.0
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/tj/go-spin v1.1.0
|
||||
go.opentelemetry.io/otel v1.16.0
|
||||
go.opentelemetry.io/otel/sdk v1.16.0
|
||||
golang.org/x/sync v0.3.0
|
||||
github.com/vmware-tanzu/velero v1.12.0
|
||||
go.opentelemetry.io/otel v1.19.0
|
||||
go.opentelemetry.io/otel/sdk v1.19.0
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9
|
||||
golang.org/x/mod v0.12.0
|
||||
golang.org/x/sync v0.4.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
k8s.io/api v0.27.4
|
||||
k8s.io/apiextensions-apiserver v0.27.2
|
||||
k8s.io/apimachinery v0.27.4
|
||||
k8s.io/apiserver v0.27.4
|
||||
k8s.io/cli-runtime v0.27.4
|
||||
k8s.io/client-go v0.27.4
|
||||
k8s.io/api v0.28.2
|
||||
k8s.io/apiextensions-apiserver v0.28.1
|
||||
k8s.io/apimachinery v0.28.2
|
||||
k8s.io/apiserver v0.28.1
|
||||
k8s.io/cli-runtime v0.28.2
|
||||
k8s.io/client-go v0.28.2
|
||||
k8s.io/klog/v2 v2.100.1
|
||||
oras.land/oras-go v1.2.3
|
||||
sigs.k8s.io/controller-runtime v0.15.1
|
||||
sigs.k8s.io/controller-runtime v0.16.2
|
||||
sigs.k8s.io/e2e-framework v0.3.0
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230106234847-43070de90fa1 // indirect
|
||||
github.com/MakeNowJust/heredoc v1.0.0 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.2.1 // indirect
|
||||
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
|
||||
github.com/Masterminds/squirrel v1.5.4 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||
github.com/chai2010/gettext-go v1.0.2 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||
github.com/docker/distribution v2.8.2+incompatible // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.10.2 // indirect
|
||||
github.com/evanphx/json-patch/v5 v5.6.0 // indirect
|
||||
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect
|
||||
github.com/go-gorp/gorp/v3 v3.0.5 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
||||
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
||||
github.com/google/s2a-go v0.1.3 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
|
||||
github.com/google/gnostic-models v0.6.8 // indirect
|
||||
github.com/google/s2a-go v0.1.7 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.1 // indirect
|
||||
github.com/gosuri/uitable v0.0.4 // indirect
|
||||
github.com/huandu/xstrings v1.4.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||
github.com/jmoiron/sqlx v1.3.5 // 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/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||
github.com/mistifyio/go-zfs/v3 v3.0.0 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||
github.com/rubenv/sql-migrate v1.3.1 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.3.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
||||
github.com/shopspring/decimal v1.3.1 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/sylabs/sif/v2 v2.11.1 // indirect
|
||||
github.com/tchap/go-patricia/v2 v2.3.1 // indirect
|
||||
github.com/vladimirvivien/gexe v0.2.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.16.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.16.0 // indirect
|
||||
go.uber.org/atomic v1.10.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
|
||||
go.opentelemetry.io/otel/metric v1.19.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.19.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // indirect
|
||||
golang.org/x/mod v0.10.0 // indirect
|
||||
golang.org/x/tools v0.9.1 // indirect
|
||||
sigs.k8s.io/e2e-framework v0.2.0 // indirect
|
||||
golang.org/x/tools v0.13.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230913181813-007df8e322eb // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230920204549-e6e6cdab5c13 // indirect
|
||||
k8s.io/component-base v0.28.1 // indirect
|
||||
k8s.io/kubectl v0.28.1 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.110.0 // indirect
|
||||
cloud.google.com/go/compute v1.19.0 // indirect
|
||||
cloud.google.com/go/iam v0.13.0 // indirect
|
||||
cloud.google.com/go/storage v1.28.1 // indirect
|
||||
cloud.google.com/go v0.110.7 // indirect
|
||||
cloud.google.com/go/compute v1.23.0 // indirect
|
||||
cloud.google.com/go/iam v1.1.1 // indirect
|
||||
cloud.google.com/go/storage v1.30.1 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||
github.com/BurntSushi/toml v1.2.1 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.0 // indirect
|
||||
github.com/Microsoft/hcsshim v0.10.0-rc.7 // indirect
|
||||
github.com/andybalholm/brotli v1.0.1 // indirect
|
||||
github.com/aws/aws-sdk-go v1.44.122 // indirect
|
||||
github.com/aws/aws-sdk-go v1.44.253 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
|
||||
github.com/c9s/goprocinfo v0.0.0-20170724085704-0010a05ce49f // indirect
|
||||
@@ -105,8 +135,8 @@ require (
|
||||
github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 // indirect
|
||||
github.com/containers/ocicrypt v1.1.7 // indirect
|
||||
github.com/containers/storage v1.46.0 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.2.3 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/docker/cli v23.0.1+incompatible // indirect
|
||||
github.com/docker/docker v23.0.3+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.7.0 // indirect
|
||||
@@ -127,11 +157,10 @@ require (
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/btree v1.0.1 // indirect
|
||||
github.com/google/gnostic v0.6.9 // indirect
|
||||
github.com/google/go-cmp v0.5.9 // indirect
|
||||
github.com/google/go-intervals v0.0.2 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.8.0 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
|
||||
github.com/gorilla/mux v1.8.0 // indirect
|
||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
@@ -144,7 +173,7 @@ require (
|
||||
github.com/jmespath/go-jmespath v0.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.16.3 // indirect
|
||||
github.com/klauspost/compress v1.17.0 // indirect
|
||||
github.com/klauspost/pgzip v1.2.6-0.20220930104621-17e8dac29df8 // indirect
|
||||
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
@@ -173,54 +202,54 @@ require (
|
||||
github.com/opencontainers/runtime-spec v1.1.0-rc.1 // indirect
|
||||
github.com/opencontainers/selinux v1.11.0 // indirect
|
||||
github.com/ostreedev/ostree-go v0.0.0-20210805093236-719684c64e4f // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.2 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_golang v1.15.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_golang v1.16.0 // indirect
|
||||
github.com/prometheus/client_model v0.4.0 // indirect
|
||||
github.com/prometheus/common v0.42.0 // indirect
|
||||
github.com/prometheus/procfs v0.9.0 // indirect
|
||||
github.com/prometheus/common v0.44.0 // indirect
|
||||
github.com/prometheus/procfs v0.10.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.4 // indirect
|
||||
github.com/spf13/afero v1.9.5 // indirect
|
||||
github.com/spf13/afero v1.10.0 // indirect
|
||||
github.com/spf13/cast v1.5.1 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/subosito/gotenv v1.4.2 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.11 // indirect
|
||||
github.com/tklauser/numcpus v0.6.0 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
||||
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||
github.com/ulikunitz/xz v0.5.11 // indirect
|
||||
github.com/vbatts/tar-split v0.11.3 // indirect
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
|
||||
github.com/xlab/treeprint v1.1.0 // indirect
|
||||
github.com/xlab/treeprint v1.2.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.3 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect
|
||||
golang.org/x/crypto v0.9.0 // indirect
|
||||
golang.org/x/net v0.10.0 // indirect
|
||||
golang.org/x/oauth2 v0.7.0 // indirect
|
||||
golang.org/x/sys v0.10.0 // indirect
|
||||
golang.org/x/term v0.8.0 // indirect
|
||||
golang.org/x/text v0.12.0
|
||||
go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect
|
||||
golang.org/x/crypto v0.14.0 // indirect
|
||||
golang.org/x/net v0.17.0
|
||||
golang.org/x/oauth2 v0.12.0 // indirect
|
||||
golang.org/x/sys v0.13.0 // indirect
|
||||
golang.org/x/term v0.13.0 // indirect
|
||||
golang.org/x/text v0.13.0
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||
google.golang.org/api v0.122.0 // indirect
|
||||
google.golang.org/api v0.143.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
|
||||
google.golang.org/grpc v1.55.0 // indirect
|
||||
google.golang.org/protobuf v1.30.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230913181813-007df8e322eb // indirect
|
||||
google.golang.org/grpc v1.58.3 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f // indirect
|
||||
k8s.io/metrics v0.27.4
|
||||
helm.sh/helm/v3 v3.12.3
|
||||
k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect
|
||||
k8s.io/metrics v0.28.2
|
||||
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2
|
||||
periph.io/x/host/v3 v3.8.2
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
|
||||
sigs.k8s.io/kustomize/api v0.13.2 // indirect
|
||||
sigs.k8s.io/kustomize/kyaml v0.14.1 // indirect
|
||||
sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3 // indirect
|
||||
sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
|
||||
sigs.k8s.io/yaml v1.3.0 // indirect
|
||||
sigs.k8s.io/yaml v1.3.0
|
||||
)
|
||||
|
||||
replace (
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package specs
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
func LoadFromConfigMap(ctx context.Context, client kubernetes.Interface, ns string, name string) (map[string]string, error) {
|
||||
foundConfigMap, err := client.CoreV1().ConfigMaps(ns).Get(ctx, name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get configmap")
|
||||
}
|
||||
|
||||
klog.V(1).InfoS("Loaded data from config map", "name",
|
||||
foundConfigMap.Name, "namespace", foundConfigMap.Namespace,
|
||||
)
|
||||
|
||||
return foundConfigMap.Data, nil
|
||||
}
|
||||
|
||||
func LoadFromConfigMapMatchingLabel(ctx context.Context, client kubernetes.Interface, label string, ns string, key string) ([]string, error) {
|
||||
var configMapMatchingKey []string
|
||||
|
||||
configMaps, err := client.CoreV1().ConfigMaps(ns).List(ctx, metav1.ListOptions{LabelSelector: label})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to search for configmaps in the cluster")
|
||||
}
|
||||
|
||||
for _, configMap := range configMaps.Items {
|
||||
spec, ok := configMap.Data[key]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
klog.V(1).InfoS("Loaded spec from config map", "name", configMap.Name,
|
||||
"namespace", configMap.Namespace, "data key", key, "label selector", label,
|
||||
)
|
||||
configMapMatchingKey = append(configMapMatchingKey, string(spec))
|
||||
}
|
||||
|
||||
return configMapMatchingKey, nil
|
||||
}
|
||||
@@ -359,7 +359,7 @@ spec:
|
||||
_, err := client.CoreV1().ConfigMaps(configmap.Namespace).Create(ctx, &configmap, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
got, err := LoadFromConfigMapMatchingLabel(client, "troubleshoot.io/kind=support-bundle", "", "support-bundle-spec")
|
||||
got, err := LoadFromConfigMapMatchingLabel(ctx, client, "troubleshoot.io/kind=support-bundle", "", "support-bundle-spec")
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
@@ -451,7 +451,7 @@ spec:
|
||||
_, err := client.CoreV1().ConfigMaps(configmap.Namespace).Create(ctx, &configmap, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
got, err := LoadFromConfigMapMatchingLabel(client, "troubleshoot.io/kind=support-bundle", "some-namespace", "support-bundle-spec")
|
||||
got, err := LoadFromConfigMapMatchingLabel(ctx, client, "troubleshoot.io/kind=support-bundle", "some-namespace", "support-bundle-spec")
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
@@ -549,7 +549,7 @@ spec:
|
||||
_, err := client.CoreV1().ConfigMaps(configmap.Namespace).Create(ctx, &configmap, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
got, err := LoadFromConfigMapMatchingLabel(client, "troubleshoot.io/kind=support-bundle", "", "redactor-spec")
|
||||
got, err := LoadFromConfigMapMatchingLabel(ctx, client, "troubleshoot.io/kind=support-bundle", "", "redactor-spec")
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
@@ -0,0 +1,42 @@
|
||||
package specs
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
func LoadFromSecret(ctx context.Context, client kubernetes.Interface, ns string, name string) (map[string][]byte, error) {
|
||||
foundSecret, err := client.CoreV1().Secrets(ns).Get(ctx, name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get secret")
|
||||
}
|
||||
|
||||
return foundSecret.Data, nil
|
||||
}
|
||||
|
||||
func LoadFromSecretMatchingLabel(ctx context.Context, client kubernetes.Interface, label string, ns string, key string) ([]string, error) {
|
||||
var secretsMatchingKey []string
|
||||
|
||||
secrets, err := client.CoreV1().Secrets(ns).List(ctx, metav1.ListOptions{LabelSelector: label})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to search for secrets in the cluster")
|
||||
}
|
||||
|
||||
for _, secret := range secrets.Items {
|
||||
spec, ok := secret.Data[key]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
klog.V(1).InfoS("Loaded spec from secret", "name", secret.Name,
|
||||
"namespace", secret.Namespace, "data key", key, "label selector", label,
|
||||
)
|
||||
secretsMatchingKey = append(secretsMatchingKey, string(spec))
|
||||
}
|
||||
|
||||
return secretsMatchingKey, nil
|
||||
}
|
||||
@@ -359,7 +359,7 @@ spec:
|
||||
_, err := client.CoreV1().Secrets(secret.Namespace).Create(ctx, &secret, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
got, err := LoadFromSecretMatchingLabel(client, "troubleshoot.io/kind=support-bundle", "", "support-bundle-spec")
|
||||
got, err := LoadFromSecretMatchingLabel(ctx, client, "troubleshoot.io/kind=support-bundle", "", "support-bundle-spec")
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
@@ -451,7 +451,7 @@ spec:
|
||||
_, err := client.CoreV1().Secrets(secret.Namespace).Create(ctx, &secret, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
got, err := LoadFromSecretMatchingLabel(client, "troubleshoot.io/kind=support-bundle", "some-namespace", "support-bundle-spec")
|
||||
got, err := LoadFromSecretMatchingLabel(ctx, client, "troubleshoot.io/kind=support-bundle", "some-namespace", "support-bundle-spec")
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
@@ -549,7 +549,7 @@ spec:
|
||||
_, err := client.CoreV1().Secrets(secret.Namespace).Create(ctx, &secret, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
got, err := LoadFromSecretMatchingLabel(client, "troubleshoot.io/kind=support-bundle", "", "redactor-spec")
|
||||
got, err := LoadFromSecretMatchingLabel(ctx, client, "troubleshoot.io/kind=support-bundle", "", "redactor-spec")
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
@@ -0,0 +1,342 @@
|
||||
package specs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/replicatedhq/troubleshoot/internal/util"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/constants"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/k8sutil"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/loader"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/oci"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/types"
|
||||
"github.com/spf13/viper"
|
||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
// SplitTroubleshootSecretLabelSelector splits a label selector into two selectors, if applicable:
|
||||
// 1. troubleshoot.io/kind=support-bundle and non-troubleshoot (if contains) labels selector.
|
||||
// 2. troubleshoot.sh/kind=support-bundle and non-troubleshoot (if contains) labels selector.
|
||||
func SplitTroubleshootSecretLabelSelector(ctx context.Context, labelSelector labels.Selector) ([]string, error) {
|
||||
|
||||
klog.V(1).Infof("Split %q selector into troubleshoot and non-troubleshoot labels selector separately, if applicable", labelSelector.String())
|
||||
|
||||
selectorRequirements, selectorSelectable := labelSelector.Requirements()
|
||||
if !selectorSelectable {
|
||||
return nil, errors.Errorf("Selector %q is not selectable", labelSelector.String())
|
||||
}
|
||||
|
||||
var troubleshootReqs, otherReqs []labels.Requirement
|
||||
|
||||
for _, req := range selectorRequirements {
|
||||
if req.Key() == constants.TroubleshootIOLabelKey || req.Key() == constants.TroubleshootSHLabelKey {
|
||||
troubleshootReqs = append(troubleshootReqs, req)
|
||||
} else {
|
||||
otherReqs = append(otherReqs, req)
|
||||
}
|
||||
}
|
||||
|
||||
parsedSelectorStrings := make([]string, 0)
|
||||
// Combine each troubleshoot requirement with other requirements to form new selectors
|
||||
s := labelSelector.String()
|
||||
if len(troubleshootReqs) == 0 && s != "" {
|
||||
return []string{s}, nil
|
||||
}
|
||||
|
||||
for _, tReq := range troubleshootReqs {
|
||||
reqs := append(otherReqs, tReq)
|
||||
newSelector := labels.NewSelector().Add(reqs...)
|
||||
parsedSelectorStrings = append(parsedSelectorStrings, newSelector.String())
|
||||
}
|
||||
|
||||
return parsedSelectorStrings, nil
|
||||
}
|
||||
|
||||
var httpClient = &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
// LoadFromCLIArgs loads troubleshoot specs from args passed to a CLI command.
|
||||
// This loader function is meant for troubleshoot CLI commands only, hence not making it public.
|
||||
// It will contain opinionated logic for CLI commands such as interpreting viper flags,
|
||||
// supporting secret/ uri format, downloading from OCI registry and other URLs, etc.
|
||||
func LoadFromCLIArgs(ctx context.Context, client kubernetes.Interface, args []string, vp *viper.Viper) (*loader.TroubleshootKinds, error) {
|
||||
// Let's always ensure we have a context
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
rawSpecs := []string{}
|
||||
|
||||
for _, v := range args {
|
||||
if strings.HasPrefix(v, "secret/") {
|
||||
// format secret/namespace-name/secret-name[/data-key]
|
||||
pathParts := strings.Split(v, "/")
|
||||
if len(pathParts) > 4 {
|
||||
return nil, types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, errors.Errorf("secret path %s must have at most 4 components", v))
|
||||
}
|
||||
if len(pathParts) < 3 {
|
||||
return nil, types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, errors.Errorf("secret path %s must have at least 3 components", v))
|
||||
}
|
||||
|
||||
data, err := LoadFromSecret(ctx, client, pathParts[1], pathParts[2])
|
||||
if err != nil {
|
||||
return nil, types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, errors.Wrap(err, "failed to get spec from secret"))
|
||||
}
|
||||
|
||||
// If we have a key defined, then load specs from that key only.
|
||||
if len(pathParts) == 4 {
|
||||
spec, ok := data[pathParts[3]]
|
||||
if ok {
|
||||
rawSpecs = append(rawSpecs, string(spec))
|
||||
}
|
||||
} else {
|
||||
// Append all data in the secret. Some may not be specs, but that's ok. They will be ignored.
|
||||
for _, spec := range data {
|
||||
rawSpecs = append(rawSpecs, string(spec))
|
||||
}
|
||||
}
|
||||
} else if strings.HasPrefix(v, "configmap/") {
|
||||
// format configmap/namespace-name/configmap-name[/data-key]
|
||||
pathParts := strings.Split(v, "/")
|
||||
if len(pathParts) > 4 {
|
||||
return nil, types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, errors.Errorf("configmap path %s must have at most 4 components", v))
|
||||
}
|
||||
if len(pathParts) < 3 {
|
||||
return nil, types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, errors.Errorf("configmap path %s must have at least 3 components", v))
|
||||
}
|
||||
|
||||
data, err := LoadFromConfigMap(ctx, client, pathParts[1], pathParts[2])
|
||||
if err != nil {
|
||||
return nil, types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, errors.Wrap(err, "failed to get spec from configmap"))
|
||||
}
|
||||
|
||||
// If we have a key defined, then load specs from that key only.
|
||||
if len(pathParts) == 4 {
|
||||
spec, ok := data[pathParts[3]]
|
||||
if ok {
|
||||
rawSpecs = append(rawSpecs, spec)
|
||||
}
|
||||
} else {
|
||||
// Append all data in the configmap. Some may not be specs, but that's ok. They will be ignored.
|
||||
for _, spec := range data {
|
||||
rawSpecs = append(rawSpecs, spec)
|
||||
}
|
||||
}
|
||||
} else if _, err := os.Stat(v); err == nil {
|
||||
b, err := os.ReadFile(v)
|
||||
if err != nil {
|
||||
return nil, types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, err)
|
||||
}
|
||||
|
||||
rawSpecs = append(rawSpecs, string(b))
|
||||
} else if v == "-" {
|
||||
b, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
return nil, types.NewExitCodeError(constants.EXIT_CODE_CATCH_ALL, err)
|
||||
}
|
||||
rawSpecs = append(rawSpecs, string(b))
|
||||
} else {
|
||||
u, err := url.Parse(v)
|
||||
if err != nil {
|
||||
return nil, types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, err)
|
||||
}
|
||||
|
||||
if u.Scheme == "oci" {
|
||||
content, err := oci.PullSpecsFromOCI(ctx, v)
|
||||
if err != nil {
|
||||
if err == oci.ErrNoRelease {
|
||||
return nil, types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, errors.Errorf("no release found for %s.\nCheck the oci:// uri for errors or contact the application vendor for support.", v))
|
||||
}
|
||||
|
||||
return nil, types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, err)
|
||||
}
|
||||
|
||||
rawSpecs = append(rawSpecs, content...)
|
||||
} else {
|
||||
if !util.IsURL(v) {
|
||||
return nil, types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, fmt.Errorf("%s is not a URL and was not found (err %s)", v, err))
|
||||
}
|
||||
|
||||
// Download preflight specs
|
||||
rawSpec, err := downloadFromHttpURL(ctx, v, map[string]string{"User-Agent": "Replicated_Preflight/v1beta2"})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rawSpecs = append(rawSpecs, rawSpec)
|
||||
|
||||
// Download support bundle specs
|
||||
rawSpec, err = downloadFromHttpURL(ctx, v, map[string]string{
|
||||
"User-Agent": "Replicated_Troubleshoot/v1beta1",
|
||||
"Bundle-Upload-Host": fmt.Sprintf("%s://%s", u.Scheme, u.Host),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rawSpecs = append(rawSpecs, rawSpec)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
kinds, err := loader.LoadSpecs(ctx, loader.LoadOptions{
|
||||
RawSpecs: rawSpecs,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if vp.GetBool("load-cluster-specs") {
|
||||
clusterKinds, err := LoadFromCluster(ctx, client, vp.GetStringSlice("selector"), vp.GetString("namespace"))
|
||||
if err != nil {
|
||||
return nil, types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, err)
|
||||
}
|
||||
|
||||
kinds.Add(clusterKinds)
|
||||
}
|
||||
|
||||
return kinds, nil
|
||||
}
|
||||
|
||||
func downloadFromHttpURL(ctx context.Context, url string, headers map[string]string) (string, error) {
|
||||
hs := []string{}
|
||||
for k, v := range headers {
|
||||
hs = append(hs, fmt.Sprintf("%s: %s", k, v))
|
||||
}
|
||||
|
||||
klog.V(1).Infof("Downloading troubleshoot specs: url=%s, headers=[%v]", url, strings.Join(hs, ", "))
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
// exit code: should this be catch all or spec issues...?
|
||||
return "", types.NewExitCodeError(constants.EXIT_CODE_CATCH_ALL, err)
|
||||
}
|
||||
for k, v := range headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
// exit code: should this be catch all or spec issues...?
|
||||
return "", types.NewExitCodeError(constants.EXIT_CODE_CATCH_ALL, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
klog.V(1).Infof("Response status: %s", resp.Status)
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, err)
|
||||
}
|
||||
return string(body), nil
|
||||
}
|
||||
|
||||
// LoadFromCluster loads troubleshoot specs from the cluster based on the provided labels.
|
||||
// By default this will be troubleshoot.io/kind=support-bundle and troubleshoot.sh/kind=support-bundle
|
||||
// labels. We search for secrets and configmaps with the label selector and extract the raw data.
|
||||
// We then load the specs from the raw data. If the user does not have sufficient permissions
|
||||
// to list & read secrets and configmaps from all namespaces, we will fallback to trying each
|
||||
// namespace individually, and eventually default to the configured kubeconfig namespace.
|
||||
func LoadFromCluster(ctx context.Context, client kubernetes.Interface, selectors []string, ns string) (*loader.TroubleshootKinds, error) {
|
||||
if reflect.DeepEqual(selectors, []string{"troubleshoot.sh/kind=support-bundle"}) {
|
||||
// Its the default selector so we append troubleshoot.io/kind=support-bundle to it due to backwards compatibility
|
||||
selectors = append(selectors, "troubleshoot.io/kind=support-bundle")
|
||||
}
|
||||
|
||||
labelSelector := strings.Join(selectors, ",")
|
||||
|
||||
parsedSelector, err := labels.Parse(labelSelector)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to parse selector")
|
||||
}
|
||||
|
||||
// List of namespaces we want to search for secrets and configmaps with support bundle specs
|
||||
namespaces := []string{}
|
||||
if ns != "" {
|
||||
// Just progress with the namespace provided
|
||||
namespaces = []string{ns}
|
||||
} else {
|
||||
// Check if I can read secrets and configmaps in all namespaces
|
||||
ican, err := k8sutil.CanIListAndGetAllSecretsAndConfigMaps(ctx, client)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to check if I can read secrets and configmaps")
|
||||
}
|
||||
klog.V(1).Infof("Can I read any secrets and configmaps: %v", ican)
|
||||
|
||||
if ican {
|
||||
// I can read secrets and configmaps in all namespaces
|
||||
// No need to iterate over all namespaces
|
||||
namespaces = []string{""}
|
||||
} else {
|
||||
// Get list of all namespaces and try to find specs from each namespace
|
||||
nsList, err := client.CoreV1().Namespaces().List(ctx, metav1.ListOptions{})
|
||||
if err != nil {
|
||||
if k8serrors.IsForbidden(err) {
|
||||
kubeconfig := k8sutil.GetKubeconfig()
|
||||
ns, _, err := kubeconfig.Namespace()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get namespace from kubeconfig")
|
||||
}
|
||||
// If we are not allowed to list namespaces, just use the default namespace
|
||||
// configured in the kubeconfig
|
||||
namespaces = []string{ns}
|
||||
} else {
|
||||
return nil, errors.Wrap(err, "failed to list namespaces")
|
||||
}
|
||||
}
|
||||
|
||||
for _, ns := range nsList.Items {
|
||||
namespaces = append(namespaces, ns.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var rawSpecs []string
|
||||
|
||||
parsedSelectorStrings, err := SplitTroubleshootSecretLabelSelector(ctx, parsedSelector)
|
||||
if err != nil {
|
||||
klog.Errorf("failed to parse troubleshoot labels selector %s", err)
|
||||
}
|
||||
|
||||
// Iteratively search for troubleshoot specs in all namespaces using the given selectors
|
||||
for _, parsedSelectorString := range parsedSelectorStrings {
|
||||
klog.V(1).Infof("Search specs from [%q] namespace using %q selector", strings.Join(namespaces, ", "), parsedSelectorString)
|
||||
for _, ns := range namespaces {
|
||||
for _, key := range []string{constants.SupportBundleKey, constants.PreflightKey, constants.RedactorKey} {
|
||||
specs, err := LoadFromSecretMatchingLabel(ctx, client, parsedSelectorString, ns, key)
|
||||
if err != nil {
|
||||
if !k8serrors.IsForbidden(err) {
|
||||
klog.Errorf("failed to load support bundle spec from secrets: %s", err)
|
||||
} else {
|
||||
klog.Warningf("Reading secrets from %q namespace forbidden", ns)
|
||||
}
|
||||
}
|
||||
rawSpecs = append(rawSpecs, specs...)
|
||||
|
||||
specs, err = LoadFromConfigMapMatchingLabel(ctx, client, parsedSelectorString, ns, key)
|
||||
if err != nil {
|
||||
if !k8serrors.IsForbidden(err) {
|
||||
klog.Errorf("failed to load support bundle spec from configmap: %s", err)
|
||||
} else {
|
||||
klog.Warningf("Reading configmaps from %q namespace forbidden", ns)
|
||||
}
|
||||
}
|
||||
rawSpecs = append(rawSpecs, specs...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load troubleshoot specs from the raw specs
|
||||
return loader.LoadSpecs(ctx, loader.LoadOptions{
|
||||
RawSpecs: rawSpecs,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
package specs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/replicatedhq/troubleshoot/internal/testutils"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/loader"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
testclient "k8s.io/client-go/kubernetes/fake"
|
||||
)
|
||||
|
||||
func Test_SplitTroubleshootSecretLabelSelector(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
selectorString string
|
||||
expectedSelectors []string
|
||||
expectedError bool
|
||||
}{
|
||||
{
|
||||
name: "Split both troubleshoot and non-troubleshoot labels",
|
||||
selectorString: "troubleshoot.io/kind=support-bundle,troubleshoot.sh/kind=support-bundle,a=b",
|
||||
expectedSelectors: []string{
|
||||
"a=b,troubleshoot.io/kind=support-bundle",
|
||||
"a=b,troubleshoot.sh/kind=support-bundle",
|
||||
},
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
name: "Split only troubleshoot.io label",
|
||||
selectorString: "troubleshoot.io/kind=support-bundle",
|
||||
expectedSelectors: []string{"troubleshoot.io/kind=support-bundle"},
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
name: "Split only troubleshoot.sh label",
|
||||
selectorString: "troubleshoot.sh/kind=support-bundle",
|
||||
expectedSelectors: []string{"troubleshoot.sh/kind=support-bundle"},
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
name: "Split only non-troubleshoot label",
|
||||
selectorString: "a=b",
|
||||
expectedSelectors: []string{"a=b"},
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
name: "No selector labels to split",
|
||||
selectorString: "",
|
||||
expectedSelectors: []string{},
|
||||
expectedError: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
selector, err := labels.Parse(tt.selectorString)
|
||||
if err != nil {
|
||||
t.Errorf("Error parsing selector string: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
gotSelectors, err := SplitTroubleshootSecretLabelSelector(context.TODO(), selector)
|
||||
if (err != nil) != tt.expectedError {
|
||||
t.Errorf("Expected error: %v, got: %v", tt.expectedError, err)
|
||||
return
|
||||
}
|
||||
|
||||
assert.ElementsMatch(t, tt.expectedSelectors, gotSelectors)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadFromCluster(t *testing.T) {
|
||||
theRedactor := troubleshootv1beta2.Redactor{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "troubleshoot.sh/v1beta2",
|
||||
Kind: "Redactor",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "redact-some-content",
|
||||
},
|
||||
Spec: troubleshootv1beta2.RedactorSpec{
|
||||
Redactors: []*troubleshootv1beta2.Redact{
|
||||
{
|
||||
Name: "redact-text-1",
|
||||
Removals: troubleshootv1beta2.Removals{
|
||||
Values: []string{"TEXT"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
selectors []string
|
||||
namespace string
|
||||
objects []runtime.Object
|
||||
want *loader.TroubleshootKinds
|
||||
}{
|
||||
{
|
||||
name: "no selectors",
|
||||
want: loader.NewTroubleshootKinds(),
|
||||
},
|
||||
{
|
||||
name: "spec in secret and default label selector",
|
||||
namespace: "bigbank",
|
||||
selectors: []string{
|
||||
"troubleshoot.sh/kind=support-bundle",
|
||||
},
|
||||
objects: []runtime.Object{
|
||||
secretObject("bigbank", map[string]string{
|
||||
"troubleshoot.io/kind": "support-bundle",
|
||||
}),
|
||||
},
|
||||
want: &loader.TroubleshootKinds{
|
||||
RedactorsV1Beta2: []troubleshootv1beta2.Redactor{theRedactor},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "spec in secret and no selector argument passed",
|
||||
namespace: "bigbank",
|
||||
objects: []runtime.Object{
|
||||
secretObject("bigbank", map[string]string{
|
||||
"troubleshoot.io/kind": "support-bundle",
|
||||
}),
|
||||
},
|
||||
want: loader.NewTroubleshootKinds(),
|
||||
},
|
||||
{
|
||||
name: "multiple specs default selector",
|
||||
namespace: "bigbank",
|
||||
selectors: []string{
|
||||
"troubleshoot.sh/kind=support-bundle",
|
||||
},
|
||||
objects: []runtime.Object{
|
||||
secretObject("bigbank", map[string]string{
|
||||
"troubleshoot.io/kind": "support-bundle",
|
||||
}),
|
||||
secretObject("bigbank", map[string]string{
|
||||
"troubleshoot.io/kind": "support-bundle",
|
||||
}),
|
||||
},
|
||||
want: &loader.TroubleshootKinds{
|
||||
RedactorsV1Beta2: []troubleshootv1beta2.Redactor{theRedactor, theRedactor},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "spec in secret but different namespace",
|
||||
namespace: "bigbank",
|
||||
objects: []runtime.Object{
|
||||
secretObject("anotherbank", map[string]string{
|
||||
"troubleshoot.io/kind": "support-bundle",
|
||||
}),
|
||||
},
|
||||
want: loader.NewTroubleshootKinds(),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := testclient.NewSimpleClientset(tt.objects...)
|
||||
got, err := LoadFromCluster(ctx, client, tt.selectors, tt.namespace)
|
||||
require.NoError(t, err)
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("got = %v, want %v", testutils.AsJSON(t, got), testutils.AsJSON(t, tt.want))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func secretObject(ns string, selectors map[string]string) runtime.Object {
|
||||
return &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: fmt.Sprintf("secret-name-%s", uuid.New().String()),
|
||||
Namespace: ns,
|
||||
Labels: selectors,
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"redactor-spec": []byte(`apiVersion: troubleshoot.sh/v1beta2
|
||||
kind: Redactor
|
||||
metadata:
|
||||
name: redact-some-content
|
||||
spec:
|
||||
redactors:
|
||||
- name: redact-text-1
|
||||
removals:
|
||||
values:
|
||||
- TEXT`),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,10 @@ import (
|
||||
|
||||
func GetTestFixture(t *testing.T, path string) string {
|
||||
t.Helper()
|
||||
p := filepath.Join("../../testdata", path)
|
||||
p := path
|
||||
if !filepath.IsAbs(path) {
|
||||
p = filepath.Join("../../testdata", path)
|
||||
}
|
||||
b, err := os.ReadFile(p)
|
||||
require.NoError(t, err)
|
||||
return string(b)
|
||||
@@ -53,10 +56,16 @@ func CreateTestFileWithData(t *testing.T, path, data string) {
|
||||
func LogJSON(t *testing.T, v interface{}) {
|
||||
t.Helper()
|
||||
|
||||
t.Log(AsJSON(t, v))
|
||||
}
|
||||
|
||||
func AsJSON(t *testing.T, v interface{}) string {
|
||||
t.Helper()
|
||||
|
||||
b, err := json.MarshalIndent(v, "", " ")
|
||||
if err != nil {
|
||||
t.Log(v)
|
||||
return fmt.Sprintf("%#v", v)
|
||||
} else {
|
||||
t.Log(string(b))
|
||||
return string(b)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/constants"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/multitype"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/redact"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
@@ -18,6 +22,8 @@ import (
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
const FILE_NOT_COLLECTED = "fileNotCollected"
|
||||
|
||||
type AnalyzeResult struct {
|
||||
IsPass bool
|
||||
IsFail bool
|
||||
@@ -218,10 +224,14 @@ func getAnalyzer(analyzer *troubleshootv1beta2.Analyze) Analyzer {
|
||||
return &AnalyzePostgres{analyzer: analyzer.Postgres}
|
||||
case analyzer.Mysql != nil:
|
||||
return &AnalyzeMysql{analyzer: analyzer.Mysql}
|
||||
case analyzer.Mssql != nil:
|
||||
return &AnalyzeMssql{analyzer: analyzer.Mssql}
|
||||
case analyzer.Redis != nil:
|
||||
return &AnalyzeRedis{analyzer: analyzer.Redis}
|
||||
case analyzer.CephStatus != nil:
|
||||
return &AnalyzeCephStatus{analyzer: analyzer.CephStatus}
|
||||
case analyzer.Velero != nil:
|
||||
return &AnalyzeVelero{analyzer: analyzer.Velero}
|
||||
case analyzer.Longhorn != nil:
|
||||
return &AnalyzeLonghorn{analyzer: analyzer.Longhorn}
|
||||
case analyzer.RegistryImages != nil:
|
||||
@@ -261,3 +271,18 @@ func DedupAnalyzers(allAnalyzers []*troubleshootv1beta2.Analyze) []*troubleshoot
|
||||
}
|
||||
return finalAnalyzers
|
||||
}
|
||||
|
||||
func stripRedactedLines(yaml []byte) []byte {
|
||||
buf := bytes.NewBuffer(yaml)
|
||||
scanner := bufio.NewScanner(buf)
|
||||
|
||||
out := []byte{}
|
||||
|
||||
for scanner.Scan() {
|
||||
line := strings.ReplaceAll(scanner.Text(), redact.MASK_TEXT, "HIDDEN")
|
||||
out = append(out, []byte(line)...)
|
||||
out = append(out, '\n')
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -2,10 +2,11 @@ package analyzer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/pkg/errors"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
)
|
||||
|
||||
func commonStatus(outcomes []*troubleshootv1beta2.Outcome, name string, iconKey string, iconURI string, readyReplicas int, exists bool, resourceType string) (*AnalyzeResult, error) {
|
||||
@@ -64,7 +65,7 @@ func commonStatus(outcomes []*troubleshootv1beta2.Outcome, name string, iconKey
|
||||
if exists == false && outcome.Warn.When != "absent" {
|
||||
result.IsFail = true
|
||||
result.Message = fmt.Sprintf("The %s %q was not found", resourceType, name)
|
||||
result.URI = outcome.Fail.URI
|
||||
result.URI = outcome.Warn.URI
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -104,7 +105,7 @@ func commonStatus(outcomes []*troubleshootv1beta2.Outcome, name string, iconKey
|
||||
if exists == false && outcome.Pass.When != "absent" {
|
||||
result.IsFail = true
|
||||
result.Message = fmt.Sprintf("The %s %q was not found", resourceType, name)
|
||||
result.URI = outcome.Fail.URI
|
||||
result.URI = outcome.Pass.URI
|
||||
return result, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ type providers struct {
|
||||
minikube bool
|
||||
rke2 bool
|
||||
k3s bool
|
||||
oke bool
|
||||
}
|
||||
|
||||
type Provider int
|
||||
@@ -45,6 +46,7 @@ const (
|
||||
minikube Provider = iota
|
||||
rke2 Provider = iota
|
||||
k3s Provider = iota
|
||||
oke Provider = iota
|
||||
)
|
||||
|
||||
type AnalyzeDistribution struct {
|
||||
@@ -125,6 +127,11 @@ func ParseNodesForProviders(nodes []corev1.Node) (providers, string) {
|
||||
foundProviders.k3s = true
|
||||
stringProvider = "k3s"
|
||||
}
|
||||
if k == "oci.oraclecloud.com/fault-domain" {
|
||||
// Based on: https://docs.oracle.com/en-us/iaas/Content/ContEng/Reference/contengsupportedlabelsusecases.htm
|
||||
foundProviders.oke = true
|
||||
stringProvider = "oke"
|
||||
}
|
||||
}
|
||||
|
||||
for k := range node.ObjectMeta.Annotations {
|
||||
@@ -326,6 +333,8 @@ func compareDistributionConditionalToActual(conditional string, actual providers
|
||||
isMatch = actual.rke2
|
||||
case k3s:
|
||||
isMatch = actual.k3s
|
||||
case oke:
|
||||
isMatch = actual.oke
|
||||
}
|
||||
|
||||
switch parts[0] {
|
||||
@@ -366,6 +375,8 @@ func mustNormalizeDistributionName(raw string) Provider {
|
||||
return rke2
|
||||
case "k3s":
|
||||
return k3s
|
||||
case "oke":
|
||||
return oke
|
||||
}
|
||||
|
||||
return unknown
|
||||
|
||||
@@ -32,6 +32,10 @@ func (a *AnalyzeHostFilesystemPerformance) Analyze(
|
||||
) ([]*AnalyzeResult, error) {
|
||||
hostAnalyzer := a.hostAnalyzer
|
||||
|
||||
result := &AnalyzeResult{
|
||||
Title: a.Title(),
|
||||
}
|
||||
|
||||
collectorName := hostAnalyzer.CollectorName
|
||||
if collectorName == "" {
|
||||
collectorName = "filesystemPerformance"
|
||||
@@ -39,16 +43,46 @@ func (a *AnalyzeHostFilesystemPerformance) Analyze(
|
||||
name := filepath.Join("host-collectors/filesystemPerformance", collectorName+".json")
|
||||
contents, err := getCollectedFileContents(name)
|
||||
if err != nil {
|
||||
if len(hostAnalyzer.Outcomes) >= 1 {
|
||||
// if the very first outcome is FILE_NOT_COLLECTED', then use that outcome
|
||||
// otherwise, return the error
|
||||
if hostAnalyzer.Outcomes[0].Fail != nil && hostAnalyzer.Outcomes[0].Fail.When == FILE_NOT_COLLECTED {
|
||||
result.IsFail = true
|
||||
result.Message = renderFSPerfOutcome(hostAnalyzer.Outcomes[0].Fail.Message, collect.FSPerfResults{})
|
||||
result.URI = hostAnalyzer.Outcomes[0].Fail.URI
|
||||
return []*AnalyzeResult{result}, nil
|
||||
}
|
||||
if hostAnalyzer.Outcomes[0].Warn != nil && hostAnalyzer.Outcomes[0].Warn.When == FILE_NOT_COLLECTED {
|
||||
result.IsWarn = true
|
||||
result.Message = renderFSPerfOutcome(hostAnalyzer.Outcomes[0].Warn.Message, collect.FSPerfResults{})
|
||||
result.URI = hostAnalyzer.Outcomes[0].Warn.URI
|
||||
return []*AnalyzeResult{result}, nil
|
||||
}
|
||||
if hostAnalyzer.Outcomes[0].Pass != nil && hostAnalyzer.Outcomes[0].Pass.When == FILE_NOT_COLLECTED {
|
||||
result.IsPass = true
|
||||
result.Message = renderFSPerfOutcome(hostAnalyzer.Outcomes[0].Pass.Message, collect.FSPerfResults{})
|
||||
result.URI = hostAnalyzer.Outcomes[0].Pass.URI
|
||||
return []*AnalyzeResult{result}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.Wrapf(err, "failed to get collected file %s", name)
|
||||
}
|
||||
|
||||
fsPerf := collect.FSPerfResults{}
|
||||
if err := json.Unmarshal(contents, &fsPerf); err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to unmarshal filesystem performance results from %s", name)
|
||||
fioResult := collect.FioResult{}
|
||||
if err := json.Unmarshal(contents, &fioResult); err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to unmarshal fio results from %s", name)
|
||||
}
|
||||
|
||||
result := &AnalyzeResult{
|
||||
Title: a.Title(),
|
||||
if len(fioResult.Jobs) == 0 {
|
||||
return nil, errors.Errorf("no jobs found in fio results from %s", name)
|
||||
}
|
||||
|
||||
fioWriteLatency := fioResult.Jobs[0].Sync
|
||||
|
||||
fsPerf := fioWriteLatency.FSPerfResults()
|
||||
if err := json.Unmarshal(contents, &fsPerf); err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to unmarshal filesystem performance results from %s", name)
|
||||
}
|
||||
|
||||
for _, outcome := range hostAnalyzer.Outcomes {
|
||||
@@ -124,6 +158,10 @@ func (a *AnalyzeHostFilesystemPerformance) Analyze(
|
||||
}
|
||||
|
||||
func compareHostFilesystemPerformanceConditionalToActual(conditional string, fsPerf collect.FSPerfResults) (res bool, err error) {
|
||||
if conditional == FILE_NOT_COLLECTED {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
parts := strings.Split(conditional, " ")
|
||||
if len(parts) != 3 {
|
||||
return false, fmt.Errorf("conditional must have exactly 3 parts, got %d", len(parts))
|
||||
@@ -179,7 +217,7 @@ func compareHostFilesystemPerformanceConditionalToActual(conditional string, fsP
|
||||
return doCompareHostFilesystemPerformance(comparator, fsPerf.P9999, desiredDuration)
|
||||
}
|
||||
|
||||
return false, fmt.Errorf("Unknown filesystem performance keyword %q", keyword)
|
||||
return false, fmt.Errorf("unknown filesystem performance keyword %q", keyword)
|
||||
}
|
||||
|
||||
func doCompareHostFilesystemPerformance(operator string, actual time.Duration, desired time.Duration) (bool, error) {
|
||||
@@ -196,7 +234,7 @@ func doCompareHostFilesystemPerformance(operator string, actual time.Duration, d
|
||||
return actual == desired, nil
|
||||
}
|
||||
|
||||
return false, fmt.Errorf("Unknown filesystem performance operator %q", operator)
|
||||
return false, fmt.Errorf("unknown filesystem performance operator %q", operator)
|
||||
}
|
||||
|
||||
func renderFSPerfOutcome(outcome string, fsPerf collect.FSPerfResults) string {
|
||||
|
||||
@@ -1,48 +1,315 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/collect"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAnalyzeHostFilesystemPerformance(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
fsPerf *collect.FSPerfResults
|
||||
fioResult string
|
||||
hostAnalyzer *troubleshootv1beta2.FilesystemPerformanceAnalyze
|
||||
result []*AnalyzeResult
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
name: "Cover",
|
||||
fsPerf: &collect.FSPerfResults{
|
||||
Min: 200 * time.Nanosecond,
|
||||
Max: time.Second,
|
||||
Average: 55 * time.Microsecond,
|
||||
P1: 1 * time.Microsecond,
|
||||
P5: 5 * time.Microsecond,
|
||||
P10: 10 * time.Microsecond,
|
||||
P20: 20 * time.Microsecond,
|
||||
P30: 30 * time.Microsecond,
|
||||
P40: 40 * time.Microsecond,
|
||||
P50: 50 * time.Microsecond,
|
||||
P60: 60 * time.Microsecond,
|
||||
P70: 70 * time.Microsecond,
|
||||
P80: 80 * time.Microsecond,
|
||||
P90: 90 * time.Microsecond,
|
||||
P95: 95 * time.Microsecond,
|
||||
P99: 99 * time.Microsecond,
|
||||
P995: 995 * time.Microsecond,
|
||||
P999: 999 * time.Microsecond,
|
||||
P9995: 5 * time.Millisecond,
|
||||
P9999: 9 * time.Millisecond,
|
||||
},
|
||||
fioResult: `{
|
||||
"fio version" : "fio-3.28",
|
||||
"timestamp" : 1691679955,
|
||||
"timestamp_ms" : 1691679955590,
|
||||
"time" : "Thu Aug 10 15:05:55 2023",
|
||||
"global options" : {
|
||||
"rw" : "write",
|
||||
"ioengine" : "sync",
|
||||
"fdatasync" : "1",
|
||||
"directory" : "/var/lib/etcd",
|
||||
"size" : "23068672",
|
||||
"bs" : "1024"
|
||||
},
|
||||
"jobs" : [
|
||||
{
|
||||
"jobname" : "fsperf",
|
||||
"groupid" : 0,
|
||||
"error" : 0,
|
||||
"eta" : 0,
|
||||
"elapsed" : 15,
|
||||
"job options" : {
|
||||
"name" : "fsperf",
|
||||
"runtime" : "120"
|
||||
},
|
||||
"read" : {
|
||||
"io_bytes" : 0,
|
||||
"io_kbytes" : 0,
|
||||
"bw_bytes" : 0,
|
||||
"bw" : 0,
|
||||
"iops" : 0.000000,
|
||||
"runtime" : 0,
|
||||
"total_ios" : 0,
|
||||
"short_ios" : 22527,
|
||||
"drop_ios" : 0,
|
||||
"slat_ns" : {
|
||||
"min" : 0,
|
||||
"max" : 0,
|
||||
"mean" : 0.000000,
|
||||
"stddev" : 0.000000,
|
||||
"N" : 0
|
||||
},
|
||||
"clat_ns" : {
|
||||
"min" : 0,
|
||||
"max" : 0,
|
||||
"mean" : 0.000000,
|
||||
"stddev" : 0.000000,
|
||||
"N" : 0
|
||||
},
|
||||
"lat_ns" : {
|
||||
"min" : 0,
|
||||
"max" : 0,
|
||||
"mean" : 0.000000,
|
||||
"stddev" : 0.000000,
|
||||
"N" : 0
|
||||
},
|
||||
"bw_min" : 0,
|
||||
"bw_max" : 0,
|
||||
"bw_agg" : 0.000000,
|
||||
"bw_mean" : 0.000000,
|
||||
"bw_dev" : 0.000000,
|
||||
"bw_samples" : 0,
|
||||
"iops_min" : 0,
|
||||
"iops_max" : 0,
|
||||
"iops_mean" : 0.000000,
|
||||
"iops_stddev" : 0.000000,
|
||||
"iops_samples" : 0
|
||||
},
|
||||
"write" : {
|
||||
"io_bytes" : 23068672,
|
||||
"io_kbytes" : 22528,
|
||||
"bw_bytes" : 1651182,
|
||||
"bw" : 1612,
|
||||
"iops" : 1612.483001,
|
||||
"runtime" : 13971,
|
||||
"total_ios" : 22528,
|
||||
"short_ios" : 0,
|
||||
"drop_ios" : 0,
|
||||
"slat_ns" : {
|
||||
"min" : 0,
|
||||
"max" : 0,
|
||||
"mean" : 0.000000,
|
||||
"stddev" : 0.000000,
|
||||
"N" : 0
|
||||
},
|
||||
"clat_ns" : {
|
||||
"min" : 200,
|
||||
"max" : 1000000000,
|
||||
"mean" : 55000,
|
||||
"stddev" : 12345.6789,
|
||||
"N" : 32400,
|
||||
"percentile" : {
|
||||
"1.000000" : 1000,
|
||||
"5.000000" : 5000,
|
||||
"10.000000" : 10000,
|
||||
"20.000000" : 20000,
|
||||
"30.000000" : 30000,
|
||||
"40.000000" : 40000,
|
||||
"50.000000" : 50000,
|
||||
"60.000000" : 60000,
|
||||
"70.000000" : 70000,
|
||||
"80.000000" : 80000,
|
||||
"90.000000" : 90000,
|
||||
"95.000000" : 95000,
|
||||
"99.000000" : 99000,
|
||||
"99.500000" : 995000,
|
||||
"99.900000" : 999000,
|
||||
"99.950000" : 5000000,
|
||||
"99.990000" : 9000000
|
||||
}
|
||||
},
|
||||
"lat_ns" : {
|
||||
"min" : 2684,
|
||||
"max" : 8710446,
|
||||
"mean" : 95169.335405,
|
||||
"stddev" : 172145.383902,
|
||||
"N" : 22528
|
||||
},
|
||||
"bw_min" : 1516,
|
||||
"bw_max" : 1706,
|
||||
"bw_agg" : 100.000000,
|
||||
"bw_mean" : 1613.629630,
|
||||
"bw_dev" : 35.708379,
|
||||
"bw_samples" : 27,
|
||||
"iops_min" : 1516,
|
||||
"iops_max" : 1706,
|
||||
"iops_mean" : 1613.629630,
|
||||
"iops_stddev" : 35.708379,
|
||||
"iops_samples" : 27
|
||||
},
|
||||
"trim" : {
|
||||
"io_bytes" : 0,
|
||||
"io_kbytes" : 0,
|
||||
"bw_bytes" : 0,
|
||||
"bw" : 0,
|
||||
"iops" : 0.000000,
|
||||
"runtime" : 0,
|
||||
"total_ios" : 0,
|
||||
"short_ios" : 0,
|
||||
"drop_ios" : 0,
|
||||
"slat_ns" : {
|
||||
"min" : 0,
|
||||
"max" : 0,
|
||||
"mean" : 0.000000,
|
||||
"stddev" : 0.000000,
|
||||
"N" : 0
|
||||
},
|
||||
"clat_ns" : {
|
||||
"min" : 0,
|
||||
"max" : 0,
|
||||
"mean" : 0.000000,
|
||||
"stddev" : 0.000000,
|
||||
"N" : 0
|
||||
},
|
||||
"lat_ns" : {
|
||||
"min" : 0,
|
||||
"max" : 0,
|
||||
"mean" : 0.000000,
|
||||
"stddev" : 0.000000,
|
||||
"N" : 0
|
||||
},
|
||||
"bw_min" : 0,
|
||||
"bw_max" : 0,
|
||||
"bw_agg" : 0.000000,
|
||||
"bw_mean" : 0.000000,
|
||||
"bw_dev" : 0.000000,
|
||||
"bw_samples" : 0,
|
||||
"iops_min" : 0,
|
||||
"iops_max" : 0,
|
||||
"iops_mean" : 0.000000,
|
||||
"iops_stddev" : 0.000000,
|
||||
"iops_samples" : 0
|
||||
},
|
||||
"sync" : {
|
||||
"total_ios" : 0,
|
||||
"lat_ns" : {
|
||||
"min" : 200,
|
||||
"max" : 1000000000,
|
||||
"mean" : 55000,
|
||||
"stddev" : 12345.6789,
|
||||
"N" : 32400,
|
||||
"percentile" : {
|
||||
"1.000000" : 1000,
|
||||
"5.000000" : 5000,
|
||||
"10.000000" : 10000,
|
||||
"20.000000" : 20000,
|
||||
"30.000000" : 30000,
|
||||
"40.000000" : 40000,
|
||||
"50.000000" : 50000,
|
||||
"60.000000" : 60000,
|
||||
"70.000000" : 70000,
|
||||
"80.000000" : 80000,
|
||||
"90.000000" : 90000,
|
||||
"95.000000" : 95000,
|
||||
"99.000000" : 99000,
|
||||
"99.500000" : 995000,
|
||||
"99.900000" : 999000,
|
||||
"99.950000" : 5000000,
|
||||
"99.990000" : 9000000
|
||||
}
|
||||
}
|
||||
},
|
||||
"job_runtime" : 13970,
|
||||
"usr_cpu" : 1.410165,
|
||||
"sys_cpu" : 5.454545,
|
||||
"ctx" : 72137,
|
||||
"majf" : 0,
|
||||
"minf" : 16,
|
||||
"iodepth_level" : {
|
||||
"1" : 199.995561,
|
||||
"2" : 0.000000,
|
||||
"4" : 0.000000,
|
||||
"8" : 0.000000,
|
||||
"16" : 0.000000,
|
||||
"32" : 0.000000,
|
||||
">=64" : 0.000000
|
||||
},
|
||||
"iodepth_submit" : {
|
||||
"0" : 0.000000,
|
||||
"4" : 100.000000,
|
||||
"8" : 0.000000,
|
||||
"16" : 0.000000,
|
||||
"32" : 0.000000,
|
||||
"64" : 0.000000,
|
||||
">=64" : 0.000000
|
||||
},
|
||||
"iodepth_complete" : {
|
||||
"0" : 0.000000,
|
||||
"4" : 100.000000,
|
||||
"8" : 0.000000,
|
||||
"16" : 0.000000,
|
||||
"32" : 0.000000,
|
||||
"64" : 0.000000,
|
||||
">=64" : 0.000000
|
||||
},
|
||||
"latency_ns" : {
|
||||
"2" : 0.000000,
|
||||
"4" : 0.000000,
|
||||
"10" : 0.000000,
|
||||
"20" : 0.000000,
|
||||
"50" : 0.000000,
|
||||
"100" : 0.000000,
|
||||
"250" : 0.000000,
|
||||
"500" : 0.000000,
|
||||
"750" : 0.000000,
|
||||
"1000" : 0.000000
|
||||
},
|
||||
"latency_us" : {
|
||||
"2" : 0.000000,
|
||||
"4" : 27.077415,
|
||||
"10" : 42.032138,
|
||||
"20" : 5.450994,
|
||||
"50" : 0.306286,
|
||||
"100" : 0.026634,
|
||||
"250" : 0.461648,
|
||||
"500" : 23.291016,
|
||||
"750" : 1.269531,
|
||||
"1000" : 0.035511
|
||||
},
|
||||
"latency_ms" : {
|
||||
"2" : 0.026634,
|
||||
"4" : 0.017756,
|
||||
"10" : 0.010000,
|
||||
"20" : 0.000000,
|
||||
"50" : 0.000000,
|
||||
"100" : 0.000000,
|
||||
"250" : 0.000000,
|
||||
"500" : 0.000000,
|
||||
"750" : 0.000000,
|
||||
"1000" : 0.000000,
|
||||
"2000" : 0.000000,
|
||||
">=2000" : 0.000000
|
||||
},
|
||||
"latency_depth" : 1,
|
||||
"latency_target" : 0,
|
||||
"latency_percentile" : 100.000000,
|
||||
"latency_window" : 0
|
||||
}
|
||||
],
|
||||
"disk_util" : [
|
||||
{
|
||||
"name" : "sda",
|
||||
"read_ios" : 5610,
|
||||
"write_ios" : 45550,
|
||||
"read_merges" : 0,
|
||||
"write_merges" : 568,
|
||||
"read_ticks" : 1863,
|
||||
"write_ticks" : 11605,
|
||||
"in_queue" : 14353,
|
||||
"util" : 99.435028
|
||||
}
|
||||
]
|
||||
}`,
|
||||
hostAnalyzer: &troubleshootv1beta2.FilesystemPerformanceAnalyze{
|
||||
CollectorName: "etcd",
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
@@ -298,12 +565,307 @@ func TestAnalyzeHostFilesystemPerformance(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "skip warn if pass first",
|
||||
fsPerf: &collect.FSPerfResults{
|
||||
P99: 9 * time.Millisecond,
|
||||
},
|
||||
fioResult: `{
|
||||
"fio version" : "fio-3.28",
|
||||
"timestamp" : 1691679955,
|
||||
"timestamp_ms" : 1691679955590,
|
||||
"time" : "Thu Aug 10 15:05:55 2023",
|
||||
"global options" : {
|
||||
"rw" : "write",
|
||||
"ioengine" : "sync",
|
||||
"fdatasync" : "1",
|
||||
"directory" : "/var/lib/etcd",
|
||||
"size" : "23068672",
|
||||
"bs" : "1024"
|
||||
},
|
||||
"jobs" : [
|
||||
{
|
||||
"jobname" : "fsperf",
|
||||
"groupid" : 0,
|
||||
"error" : 0,
|
||||
"eta" : 0,
|
||||
"elapsed" : 15,
|
||||
"job options" : {
|
||||
"name" : "fsperf",
|
||||
"runtime" : "120"
|
||||
},
|
||||
"read" : {
|
||||
"io_bytes" : 0,
|
||||
"io_kbytes" : 0,
|
||||
"bw_bytes" : 0,
|
||||
"bw" : 0,
|
||||
"iops" : 0.000000,
|
||||
"runtime" : 0,
|
||||
"total_ios" : 0,
|
||||
"short_ios" : 22527,
|
||||
"drop_ios" : 0,
|
||||
"slat_ns" : {
|
||||
"min" : 0,
|
||||
"max" : 0,
|
||||
"mean" : 0.000000,
|
||||
"stddev" : 0.000000,
|
||||
"N" : 0
|
||||
},
|
||||
"clat_ns" : {
|
||||
"min" : 0,
|
||||
"max" : 0,
|
||||
"mean" : 0.000000,
|
||||
"stddev" : 0.000000,
|
||||
"N" : 0
|
||||
},
|
||||
"lat_ns" : {
|
||||
"min" : 0,
|
||||
"max" : 0,
|
||||
"mean" : 0.000000,
|
||||
"stddev" : 0.000000,
|
||||
"N" : 0
|
||||
},
|
||||
"bw_min" : 0,
|
||||
"bw_max" : 0,
|
||||
"bw_agg" : 0.000000,
|
||||
"bw_mean" : 0.000000,
|
||||
"bw_dev" : 0.000000,
|
||||
"bw_samples" : 0,
|
||||
"iops_min" : 0,
|
||||
"iops_max" : 0,
|
||||
"iops_mean" : 0.000000,
|
||||
"iops_stddev" : 0.000000,
|
||||
"iops_samples" : 0
|
||||
},
|
||||
"write" : {
|
||||
"io_bytes" : 23068672,
|
||||
"io_kbytes" : 22528,
|
||||
"bw_bytes" : 1651182,
|
||||
"bw" : 1612,
|
||||
"iops" : 1612.483001,
|
||||
"runtime" : 13971,
|
||||
"total_ios" : 22528,
|
||||
"short_ios" : 0,
|
||||
"drop_ios" : 0,
|
||||
"slat_ns" : {
|
||||
"min" : 0,
|
||||
"max" : 0,
|
||||
"mean" : 0.000000,
|
||||
"stddev" : 0.000000,
|
||||
"N" : 0
|
||||
},
|
||||
"clat_ns" : {
|
||||
"min" : 200,
|
||||
"max" : 1000000000,
|
||||
"mean" : 55000,
|
||||
"stddev" : 12345.6789,
|
||||
"N" : 32400,
|
||||
"percentile" : {
|
||||
"1.000000" : 1000,
|
||||
"5.000000" : 5000,
|
||||
"10.000000" : 10000,
|
||||
"20.000000" : 20000,
|
||||
"30.000000" : 30000,
|
||||
"40.000000" : 40000,
|
||||
"50.000000" : 50000,
|
||||
"60.000000" : 60000,
|
||||
"70.000000" : 70000,
|
||||
"80.000000" : 80000,
|
||||
"90.000000" : 90000,
|
||||
"95.000000" : 95000,
|
||||
"99.000000" : 99000,
|
||||
"99.500000" : 995000,
|
||||
"99.900000" : 999000,
|
||||
"99.950000" : 5000000,
|
||||
"99.990000" : 9000000
|
||||
}
|
||||
},
|
||||
"lat_ns" : {
|
||||
"min" : 2684,
|
||||
"max" : 8710446,
|
||||
"mean" : 95169.335405,
|
||||
"stddev" : 172145.383902,
|
||||
"N" : 22528
|
||||
},
|
||||
"bw_min" : 1516,
|
||||
"bw_max" : 1706,
|
||||
"bw_agg" : 100.000000,
|
||||
"bw_mean" : 1613.629630,
|
||||
"bw_dev" : 35.708379,
|
||||
"bw_samples" : 27,
|
||||
"iops_min" : 1516,
|
||||
"iops_max" : 1706,
|
||||
"iops_mean" : 1613.629630,
|
||||
"iops_stddev" : 35.708379,
|
||||
"iops_samples" : 27
|
||||
},
|
||||
"trim" : {
|
||||
"io_bytes" : 0,
|
||||
"io_kbytes" : 0,
|
||||
"bw_bytes" : 0,
|
||||
"bw" : 0,
|
||||
"iops" : 0.000000,
|
||||
"runtime" : 0,
|
||||
"total_ios" : 0,
|
||||
"short_ios" : 0,
|
||||
"drop_ios" : 0,
|
||||
"slat_ns" : {
|
||||
"min" : 0,
|
||||
"max" : 0,
|
||||
"mean" : 0.000000,
|
||||
"stddev" : 0.000000,
|
||||
"N" : 0
|
||||
},
|
||||
"clat_ns" : {
|
||||
"min" : 0,
|
||||
"max" : 0,
|
||||
"mean" : 0.000000,
|
||||
"stddev" : 0.000000,
|
||||
"N" : 0
|
||||
},
|
||||
"lat_ns" : {
|
||||
"min" : 0,
|
||||
"max" : 0,
|
||||
"mean" : 0.000000,
|
||||
"stddev" : 0.000000,
|
||||
"N" : 0
|
||||
},
|
||||
"bw_min" : 0,
|
||||
"bw_max" : 0,
|
||||
"bw_agg" : 0.000000,
|
||||
"bw_mean" : 0.000000,
|
||||
"bw_dev" : 0.000000,
|
||||
"bw_samples" : 0,
|
||||
"iops_min" : 0,
|
||||
"iops_max" : 0,
|
||||
"iops_mean" : 0.000000,
|
||||
"iops_stddev" : 0.000000,
|
||||
"iops_samples" : 0
|
||||
},
|
||||
"sync" : {
|
||||
"total_ios" : 0,
|
||||
"lat_ns" : {
|
||||
"min" : 200,
|
||||
"max" : 1000000000,
|
||||
"mean" : 55000,
|
||||
"stddev" : 12345.6789,
|
||||
"N" : 32400,
|
||||
"percentile" : {
|
||||
"1.000000" : 1000,
|
||||
"5.000000" : 5000,
|
||||
"10.000000" : 10000,
|
||||
"20.000000" : 20000,
|
||||
"30.000000" : 30000,
|
||||
"40.000000" : 40000,
|
||||
"50.000000" : 50000,
|
||||
"60.000000" : 60000,
|
||||
"70.000000" : 70000,
|
||||
"80.000000" : 80000,
|
||||
"90.000000" : 90000,
|
||||
"95.000000" : 95000,
|
||||
"99.000000" : 9000000,
|
||||
"99.500000" : 995000,
|
||||
"99.900000" : 999000,
|
||||
"99.950000" : 5000000,
|
||||
"99.990000" : 9000000
|
||||
}
|
||||
}
|
||||
},
|
||||
"job_runtime" : 13970,
|
||||
"usr_cpu" : 1.410165,
|
||||
"sys_cpu" : 5.454545,
|
||||
"ctx" : 72137,
|
||||
"majf" : 0,
|
||||
"minf" : 16,
|
||||
"iodepth_level" : {
|
||||
"1" : 199.995561,
|
||||
"2" : 0.000000,
|
||||
"4" : 0.000000,
|
||||
"8" : 0.000000,
|
||||
"16" : 0.000000,
|
||||
"32" : 0.000000,
|
||||
">=64" : 0.000000
|
||||
},
|
||||
"iodepth_submit" : {
|
||||
"0" : 0.000000,
|
||||
"4" : 100.000000,
|
||||
"8" : 0.000000,
|
||||
"16" : 0.000000,
|
||||
"32" : 0.000000,
|
||||
"64" : 0.000000,
|
||||
">=64" : 0.000000
|
||||
},
|
||||
"iodepth_complete" : {
|
||||
"0" : 0.000000,
|
||||
"4" : 100.000000,
|
||||
"8" : 0.000000,
|
||||
"16" : 0.000000,
|
||||
"32" : 0.000000,
|
||||
"64" : 0.000000,
|
||||
">=64" : 0.000000
|
||||
},
|
||||
"latency_ns" : {
|
||||
"2" : 0.000000,
|
||||
"4" : 0.000000,
|
||||
"10" : 0.000000,
|
||||
"20" : 0.000000,
|
||||
"50" : 0.000000,
|
||||
"100" : 0.000000,
|
||||
"250" : 0.000000,
|
||||
"500" : 0.000000,
|
||||
"750" : 0.000000,
|
||||
"1000" : 0.000000
|
||||
},
|
||||
"latency_us" : {
|
||||
"2" : 0.000000,
|
||||
"4" : 27.077415,
|
||||
"10" : 42.032138,
|
||||
"20" : 5.450994,
|
||||
"50" : 0.306286,
|
||||
"100" : 0.026634,
|
||||
"250" : 0.461648,
|
||||
"500" : 23.291016,
|
||||
"750" : 1.269531,
|
||||
"1000" : 0.035511
|
||||
},
|
||||
"latency_ms" : {
|
||||
"2" : 0.026634,
|
||||
"4" : 0.017756,
|
||||
"10" : 0.010000,
|
||||
"20" : 0.000000,
|
||||
"50" : 0.000000,
|
||||
"100" : 0.000000,
|
||||
"250" : 0.000000,
|
||||
"500" : 0.000000,
|
||||
"750" : 0.000000,
|
||||
"1000" : 0.000000,
|
||||
"2000" : 0.000000,
|
||||
">=2000" : 0.000000
|
||||
},
|
||||
"latency_depth" : 1,
|
||||
"latency_target" : 0,
|
||||
"latency_percentile" : 100.000000,
|
||||
"latency_window" : 0
|
||||
}
|
||||
],
|
||||
"disk_util" : [
|
||||
{
|
||||
"name" : "sda",
|
||||
"read_ios" : 5610,
|
||||
"write_ios" : 45550,
|
||||
"read_merges" : 0,
|
||||
"write_merges" : 568,
|
||||
"read_ticks" : 1863,
|
||||
"write_ticks" : 11605,
|
||||
"in_queue" : 14353,
|
||||
"util" : 99.435028
|
||||
}
|
||||
]
|
||||
}`,
|
||||
hostAnalyzer: &troubleshootv1beta2.FilesystemPerformanceAnalyze{
|
||||
CollectorName: "file system performance",
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Warn: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "fileNotCollected",
|
||||
Message: "Warn when the file was not collected",
|
||||
},
|
||||
},
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "p99 < 10ms",
|
||||
@@ -332,27 +894,202 @@ func TestAnalyzeHostFilesystemPerformance(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "bail if malformed JSON",
|
||||
fioResult: `{
|
||||
bad JSON
|
||||
}`,
|
||||
hostAnalyzer: &troubleshootv1beta2.FilesystemPerformanceAnalyze{
|
||||
CollectorName: "file system performance",
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
Message: "bad JSON should not be analyzed",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
name: "bail if fio ran no jobs",
|
||||
fioResult: `{
|
||||
"fio version" : "fio-3.28",
|
||||
"timestamp" : 1691679955,
|
||||
"timestamp_ms" : 1691679955590,
|
||||
"time" : "Thu Aug 10 15:05:55 2023",
|
||||
"global options" : {
|
||||
"rw" : "write",
|
||||
"ioengine" : "sync",
|
||||
"fdatasync" : "1",
|
||||
"directory" : "/var/lib/etcd",
|
||||
"size" : "23068672",
|
||||
"bs" : "1024"
|
||||
},
|
||||
"jobs" : [
|
||||
]
|
||||
}`,
|
||||
hostAnalyzer: &troubleshootv1beta2.FilesystemPerformanceAnalyze{
|
||||
CollectorName: "file system performance",
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
Message: "an empty Jobs array should not be analyzed",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
req := require.New(t)
|
||||
b, err := json.Marshal(test.fsPerf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
b := []byte(test.fioResult)
|
||||
|
||||
getCollectedFileContents := func(filename string) ([]byte, error) {
|
||||
return b, nil
|
||||
}
|
||||
|
||||
result, err := (&AnalyzeHostFilesystemPerformance{test.hostAnalyzer}).Analyze(getCollectedFileContents, nil)
|
||||
a := AnalyzeHostFilesystemPerformance{test.hostAnalyzer}
|
||||
result, err := a.Analyze(getCollectedFileContents, nil)
|
||||
if test.expectErr {
|
||||
req.Error(err)
|
||||
} else {
|
||||
req.NoError(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, test.result, result)
|
||||
req.Equal(test.result, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnalyzeHostFilesystemPerformanceNoFile(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
hostAnalyzer *troubleshootv1beta2.FilesystemPerformanceAnalyze
|
||||
result []*AnalyzeResult
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
name: "default behavior",
|
||||
hostAnalyzer: &troubleshootv1beta2.FilesystemPerformanceAnalyze{
|
||||
CollectorName: "irrel",
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
Message: "a nonexistent file should not be analyzed",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
name: "fail case",
|
||||
hostAnalyzer: &troubleshootv1beta2.FilesystemPerformanceAnalyze{
|
||||
CollectorName: "irrel",
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "fileNotCollected",
|
||||
Message: "the required file was not collected",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectErr: false,
|
||||
result: []*AnalyzeResult{
|
||||
{
|
||||
Title: "Filesystem Performance",
|
||||
IsFail: true,
|
||||
Message: "the required file was not collected",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "warn case",
|
||||
hostAnalyzer: &troubleshootv1beta2.FilesystemPerformanceAnalyze{
|
||||
CollectorName: "irrel",
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Warn: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "fileNotCollected",
|
||||
Message: "the required file was not collected",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectErr: false,
|
||||
result: []*AnalyzeResult{
|
||||
{
|
||||
Title: "Filesystem Performance",
|
||||
IsWarn: true,
|
||||
Message: "the required file was not collected",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "pass case",
|
||||
hostAnalyzer: &troubleshootv1beta2.FilesystemPerformanceAnalyze{
|
||||
CollectorName: "irrel",
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "fileNotCollected",
|
||||
Message: "the required file was not collected",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectErr: false,
|
||||
result: []*AnalyzeResult{
|
||||
{
|
||||
Title: "Filesystem Performance",
|
||||
IsPass: true,
|
||||
Message: "the required file was not collected",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "fileNotCollected is only tested if it is the first result",
|
||||
hostAnalyzer: &troubleshootv1beta2.FilesystemPerformanceAnalyze{
|
||||
CollectorName: "irrel",
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
Message: "a nonexistent file should not be analyzed",
|
||||
},
|
||||
},
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "fileNotCollected",
|
||||
Message: "the required file was not collected",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
req := require.New(t)
|
||||
|
||||
getCollectedFileContents := func(filename string) ([]byte, error) {
|
||||
return nil, fmt.Errorf("file not found")
|
||||
}
|
||||
|
||||
a := AnalyzeHostFilesystemPerformance{test.hostAnalyzer}
|
||||
result, err := a.Analyze(getCollectedFileContents, nil)
|
||||
if test.expectErr {
|
||||
req.Error(err)
|
||||
} else {
|
||||
req.NoError(err)
|
||||
}
|
||||
|
||||
req.Equal(test.result, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/collect"
|
||||
longhornv1beta1 "github.com/replicatedhq/troubleshoot/pkg/longhorn/apis/longhorn/v1beta1"
|
||||
longhorntypes "github.com/replicatedhq/troubleshoot/pkg/longhorn/types"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/redact"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
@@ -241,21 +237,6 @@ func analyzeLonghornEngine(engine *longhornv1beta1.Engine) *AnalyzeResult {
|
||||
return result
|
||||
}
|
||||
|
||||
func stripRedactedLines(yaml []byte) []byte {
|
||||
buf := bytes.NewBuffer(yaml)
|
||||
scanner := bufio.NewScanner(buf)
|
||||
|
||||
out := []byte{}
|
||||
|
||||
for scanner.Scan() {
|
||||
line := strings.ReplaceAll(scanner.Text(), redact.MASK_TEXT, "HIDDEN")
|
||||
out = append(out, []byte(line)...)
|
||||
out = append(out, '\n')
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func analyzeLonghornReplicaChecksums(volumeName string, checksums []map[string]string) *AnalyzeResult {
|
||||
result := &AnalyzeResult{
|
||||
Title: fmt.Sprintf("Longhorn Volume Replica Corruption: %s", volumeName),
|
||||
|
||||
+34
-3
@@ -13,6 +13,39 @@ import (
|
||||
"github.com/replicatedhq/troubleshoot/pkg/collect"
|
||||
)
|
||||
|
||||
type AnalyzeMssql struct {
|
||||
analyzer *troubleshootv1beta2.DatabaseAnalyze
|
||||
}
|
||||
|
||||
func (a *AnalyzeMssql) Title() string {
|
||||
title := a.analyzer.CheckName
|
||||
if title == "" {
|
||||
title = a.collectorName()
|
||||
}
|
||||
|
||||
return title
|
||||
}
|
||||
|
||||
func (a *AnalyzeMssql) IsExcluded() (bool, error) {
|
||||
return isExcluded(a.analyzer.Exclude)
|
||||
}
|
||||
|
||||
func (a *AnalyzeMssql) Analyze(getFile getCollectedFileContents, findFiles getChildCollectedFileContents) ([]*AnalyzeResult, error) {
|
||||
result, err := analyzeMssql(a.analyzer, getFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.Strict = a.analyzer.Strict.BoolOrDefaultFalse()
|
||||
return []*AnalyzeResult{result}, nil
|
||||
}
|
||||
|
||||
func (a *AnalyzeMssql) collectorName() string {
|
||||
if a.analyzer.CollectorName != "" {
|
||||
return a.analyzer.CollectorName
|
||||
}
|
||||
return "mssql"
|
||||
}
|
||||
|
||||
func compareMssqlConditionalToActual(conditional string, result *collect.DatabaseConnection) (bool, error) {
|
||||
parts := strings.Split(strings.TrimSpace(conditional), " ")
|
||||
|
||||
@@ -90,9 +123,7 @@ func analyzeMssql(analyzer *troubleshootv1beta2.DatabaseAnalyze, getCollectedFil
|
||||
}
|
||||
|
||||
result := &AnalyzeResult{
|
||||
Title: title,
|
||||
IconKey: "kubernetes_sqlserver_analyze",
|
||||
IconURI: "https://troubleshoot.sh/images/analyzer-icons/sqlserver-analyze.svg",
|
||||
Title: title,
|
||||
}
|
||||
|
||||
for _, outcome := range analyzer.Outcomes {
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/collect"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -115,3 +118,109 @@ func Test_compareMssqlConditionalToActual(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnalyzeMssql_Analyze(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
analyzer *troubleshootv1beta2.DatabaseAnalyze
|
||||
want []*AnalyzeResult
|
||||
data map[string]any
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "mssql analyze with passing condition",
|
||||
data: map[string]any{
|
||||
"isConnected": true,
|
||||
"version": "15.0.2000.1565",
|
||||
},
|
||||
analyzer: &troubleshootv1beta2.DatabaseAnalyze{
|
||||
AnalyzeMeta: troubleshootv1beta2.AnalyzeMeta{
|
||||
CheckName: "Must be SQLServer 15.x or later",
|
||||
},
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
Message: "The SQLServer connection checks out",
|
||||
},
|
||||
},
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "connected == false",
|
||||
Message: "Cannot connect to SQLServer",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []*AnalyzeResult{
|
||||
{
|
||||
Title: "Must be SQLServer 15.x or later",
|
||||
Message: "The SQLServer connection checks out",
|
||||
IsPass: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mssql analyze with failing condition",
|
||||
data: map[string]any{
|
||||
"isConnected": true,
|
||||
"version": "14.0.2000.1565",
|
||||
},
|
||||
analyzer: &troubleshootv1beta2.DatabaseAnalyze{
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "version < 15.x",
|
||||
Message: "The SQLServer must be at least version 15",
|
||||
},
|
||||
},
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
Message: "The SQLServer connection checks out",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []*AnalyzeResult{
|
||||
{
|
||||
Title: "mssql",
|
||||
Message: "The SQLServer must be at least version 15",
|
||||
IsFail: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
a := &AnalyzeMssql{
|
||||
analyzer: tt.analyzer,
|
||||
}
|
||||
|
||||
getFile := func(_ string) ([]byte, error) {
|
||||
return json.Marshal(tt.data)
|
||||
}
|
||||
|
||||
got, err := a.Analyze(getFile, nil)
|
||||
assert.Equalf(t, tt.wantErr, err != nil, "got error = %v, wantErr %v", err, tt.wantErr)
|
||||
|
||||
got2 := fromPointerSlice(got)
|
||||
want2 := fromPointerSlice(tt.want)
|
||||
if !reflect.DeepEqual(got2, want2) {
|
||||
t.Errorf("got = %v, want %v", toJSON(got2), toJSON(want2))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func fromPointerSlice(in []*AnalyzeResult) []AnalyzeResult {
|
||||
out := make([]AnalyzeResult, len(in))
|
||||
for i := range in {
|
||||
out[i] = *in[i]
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func toJSON(in any) string {
|
||||
out, _ := json.MarshalIndent(in, "", " ")
|
||||
return string(out)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// ResticRepositorySpec is the specification for a ResticRepository.
|
||||
type ResticRepositorySpec struct {
|
||||
// VolumeNamespace is the namespace this restic repository contains
|
||||
// pod volume backups for.
|
||||
VolumeNamespace string `json:"volumeNamespace"`
|
||||
|
||||
// BackupStorageLocation is the name of the BackupStorageLocation
|
||||
// that should contain this repository.
|
||||
BackupStorageLocation string `json:"backupStorageLocation"`
|
||||
|
||||
// ResticIdentifier is the full restic-compatible string for identifying
|
||||
// this repository.
|
||||
ResticIdentifier string `json:"resticIdentifier"`
|
||||
|
||||
// MaintenanceFrequency is how often maintenance should be run.
|
||||
MaintenanceFrequency metav1.Duration `json:"maintenanceFrequency"`
|
||||
}
|
||||
|
||||
// ResticRepositoryPhase represents the lifecycle phase of a ResticRepository.
|
||||
// +kubebuilder:validation:Enum=New;Ready;NotReady
|
||||
type ResticRepositoryPhase string
|
||||
|
||||
const (
|
||||
ResticRepositoryPhaseNew ResticRepositoryPhase = "New"
|
||||
ResticRepositoryPhaseReady ResticRepositoryPhase = "Ready"
|
||||
ResticRepositoryPhaseNotReady ResticRepositoryPhase = "NotReady"
|
||||
)
|
||||
|
||||
// ResticRepositoryStatus is the current status of a ResticRepository.
|
||||
type ResticRepositoryStatus struct {
|
||||
// Phase is the current state of the ResticRepository.
|
||||
// +optional
|
||||
Phase ResticRepositoryPhase `json:"phase,omitempty"`
|
||||
|
||||
// Message is a message about the current status of the ResticRepository.
|
||||
// +optional
|
||||
Message string `json:"message,omitempty"`
|
||||
|
||||
// LastMaintenanceTime is the last time maintenance was run.
|
||||
// +optional
|
||||
// +nullable
|
||||
LastMaintenanceTime *metav1.Time `json:"lastMaintenanceTime,omitempty"`
|
||||
}
|
||||
|
||||
type ResticRepository struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
|
||||
// +optional
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
// +optional
|
||||
Spec ResticRepositorySpec `json:"spec,omitempty"`
|
||||
|
||||
// +optional
|
||||
Status ResticRepositoryStatus `json:"status,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,735 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
appsV1 "k8s.io/api/apps/v1"
|
||||
|
||||
restic_types "github.com/replicatedhq/troubleshoot/pkg/analyze/types"
|
||||
"golang.org/x/mod/semver"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
)
|
||||
|
||||
type AnalyzeVelero struct {
|
||||
analyzer *troubleshootv1beta2.VeleroAnalyze
|
||||
}
|
||||
|
||||
func (a *AnalyzeVelero) Title() string {
|
||||
title := a.analyzer.CheckName
|
||||
if title == "" {
|
||||
title = "Velero"
|
||||
}
|
||||
|
||||
return title
|
||||
}
|
||||
|
||||
func (a *AnalyzeVelero) IsExcluded() (bool, error) {
|
||||
return isExcluded(a.analyzer.Exclude)
|
||||
}
|
||||
|
||||
func (a *AnalyzeVelero) Analyze(getFile getCollectedFileContents, findFiles getChildCollectedFileContents) ([]*AnalyzeResult, error) {
|
||||
results, err := a.veleroStatus(a.analyzer, getFile, findFiles)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := range results {
|
||||
results[i].Strict = a.analyzer.Strict.BoolOrDefaultFalse()
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (a *AnalyzeVelero) veleroStatus(analyzer *troubleshootv1beta2.VeleroAnalyze, getFileContents getCollectedFileContents, findFiles getChildCollectedFileContents) ([]*AnalyzeResult, error) {
|
||||
excludeFiles := []string{}
|
||||
results := []*AnalyzeResult{}
|
||||
|
||||
oldVeleroRepoType := false
|
||||
veleroVersion, err := getVeleroVersion(excludeFiles, findFiles)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Unable to find velero deployment")
|
||||
}
|
||||
|
||||
// Check if the version string is valid erer
|
||||
if !semver.IsValid(veleroVersion) {
|
||||
return nil, errors.Errorf("Invalid velero semver: %s", veleroVersion)
|
||||
}
|
||||
|
||||
// check if veleroVersion is less than 1.10.x
|
||||
compareResult := semver.Compare(veleroVersion, "1.10.0")
|
||||
if compareResult < 0 {
|
||||
oldVeleroRepoType = true
|
||||
}
|
||||
|
||||
if oldVeleroRepoType == true {
|
||||
// old velero (v1.9.x) has a BackupRepositoryTypeRestic
|
||||
// get resticrepositories.velero.io
|
||||
resticRepositoriesDir := GetVeleroResticRepositoriesDirectory()
|
||||
resticRepositoriesGlob := filepath.Join(resticRepositoriesDir, "*.json")
|
||||
resticRepositoriesJson, err := findFiles(resticRepositoriesGlob, excludeFiles)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to find velero restic repositories files under %s", resticRepositoriesDir)
|
||||
}
|
||||
resticRepositories := []*restic_types.ResticRepository{}
|
||||
for key, resticRepositoryJson := range resticRepositoriesJson {
|
||||
var resticRepositoryArray []*restic_types.ResticRepository
|
||||
err := json.Unmarshal(resticRepositoryJson, &resticRepositories)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to unmarshal restic repository json from %s", key)
|
||||
}
|
||||
resticRepositories = append(resticRepositories, resticRepositoryArray...)
|
||||
}
|
||||
results = append(results, analyzeResticRepositories(resticRepositories)...)
|
||||
|
||||
} else {
|
||||
|
||||
// velerov1.Version
|
||||
// get backuprepositories.velero.io
|
||||
backupRepositoriesDir := GetVeleroBackupRepositoriesDirectory()
|
||||
backupRepositoriesGlob := filepath.Join(backupRepositoriesDir, "*.json")
|
||||
backupRepositoriesJson, err := findFiles(backupRepositoriesGlob, excludeFiles)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to find velero backup repositories files under %s", backupRepositoriesDir)
|
||||
}
|
||||
backupRepositories := []*velerov1.BackupRepository{}
|
||||
for key, backupRepositoryJson := range backupRepositoriesJson {
|
||||
var backupRepositoryArray []*velerov1.BackupRepository
|
||||
err := json.Unmarshal(backupRepositoryJson, &backupRepositoryArray)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to unmarshal backup repository json from %s", key)
|
||||
}
|
||||
backupRepositories = append(backupRepositories, backupRepositoryArray...)
|
||||
}
|
||||
results = append(results, analyzeBackupRepositories(backupRepositories)...)
|
||||
|
||||
}
|
||||
|
||||
// get backups.velero.io
|
||||
backupsDir := GetVeleroBackupsDirectory()
|
||||
backupsGlob := filepath.Join(backupsDir, "*.json")
|
||||
veleroJSONs, err := findFiles(backupsGlob, excludeFiles)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to find velero backup files")
|
||||
}
|
||||
backups := []*velerov1.Backup{}
|
||||
for _, veleroJSON := range veleroJSONs {
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to read velero backup file %s", veleroJSON)
|
||||
}
|
||||
var veleroBackups []*velerov1.Backup
|
||||
err = json.Unmarshal(veleroJSON, &veleroBackups)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to unmarshal velero backup file %s", veleroJSON)
|
||||
}
|
||||
backups = append(backups, veleroBackups...)
|
||||
}
|
||||
|
||||
// get backupstoragelocations.velero.io
|
||||
backupStorageLocationsDir := GetVeleroBackupStorageLocationsDirectory()
|
||||
backupStorageLocationsGlob := filepath.Join(backupStorageLocationsDir, "*.json")
|
||||
backupStorageLocationsJson, err := findFiles(backupStorageLocationsGlob, excludeFiles)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to find velero backup storage locations files under %s", backupStorageLocationsDir)
|
||||
}
|
||||
backupStorageLocations := []*velerov1.BackupStorageLocation{}
|
||||
for key, backupStorageLocationJson := range backupStorageLocationsJson {
|
||||
var backupStorageLocationArray []*velerov1.BackupStorageLocation
|
||||
err := json.Unmarshal(backupStorageLocationJson, &backupStorageLocationArray)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to unmarshal backup storage location json from %s", key)
|
||||
}
|
||||
backupStorageLocations = append(backupStorageLocations, backupStorageLocationArray...)
|
||||
}
|
||||
|
||||
// get deletebackuprequests.velero.io
|
||||
deleteBackupRequestsDir := GetVeleroDeleteBackupRequestsDirectory()
|
||||
deleteBackupRequestsGlob := filepath.Join(deleteBackupRequestsDir, "*.json")
|
||||
deleteBackupRequestsJson, err := findFiles(deleteBackupRequestsGlob, excludeFiles)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to find velero delete backup requests files under %s", deleteBackupRequestsDir)
|
||||
}
|
||||
deleteBackupRequests := []*velerov1.DeleteBackupRequest{}
|
||||
for key, deleteBackupRequestJson := range deleteBackupRequestsJson {
|
||||
var deleteBackupRequestArray []*velerov1.DeleteBackupRequest
|
||||
err := json.Unmarshal(deleteBackupRequestJson, &deleteBackupRequestArray)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to unmarshal delete backup request json from %s", key)
|
||||
}
|
||||
deleteBackupRequests = append(deleteBackupRequests, deleteBackupRequestArray...)
|
||||
}
|
||||
|
||||
// get podvolumebackups.velero.io
|
||||
podVolumeBackupsDir := GetVeleroPodVolumeBackupsDirectory()
|
||||
podVolumeBackupsGlob := filepath.Join(podVolumeBackupsDir, "*.json")
|
||||
podVolumeBackupsJson, err := findFiles(podVolumeBackupsGlob, excludeFiles)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to find velero pod volume backups files under %s", podVolumeBackupsDir)
|
||||
}
|
||||
podVolumeBackups := []*velerov1.PodVolumeBackup{}
|
||||
for key, podVolumeBackupJson := range podVolumeBackupsJson {
|
||||
var podVolumeBackupArray []*velerov1.PodVolumeBackup
|
||||
err := json.Unmarshal(podVolumeBackupJson, &podVolumeBackupArray)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to unmarshal pod volume backup json from %s", key)
|
||||
}
|
||||
podVolumeBackups = append(podVolumeBackups, podVolumeBackupArray...)
|
||||
}
|
||||
|
||||
// get podvolumerestores.velero.io
|
||||
podVolumeRestoresDir := GetVeleroPodVolumeRestoresDirectory()
|
||||
podVolumeRestoresGlob := filepath.Join(podVolumeRestoresDir, "*.json")
|
||||
podVolumeRestoresJson, err := findFiles(podVolumeRestoresGlob, excludeFiles)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to find velero pod volume restores files under %s", podVolumeRestoresDir)
|
||||
}
|
||||
podVolumeRestores := []*velerov1.PodVolumeRestore{}
|
||||
for key, podVolumeRestoreJson := range podVolumeRestoresJson {
|
||||
var podVolumeRestoreArray []*velerov1.PodVolumeRestore
|
||||
err := json.Unmarshal(podVolumeRestoreJson, &podVolumeRestoreArray)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to unmarshal pod volume restore json from %s", key)
|
||||
}
|
||||
podVolumeRestores = append(podVolumeRestores, podVolumeRestoreArray...)
|
||||
}
|
||||
|
||||
// get restores.velero.io
|
||||
restoresDir := GetVeleroRestoresDirectory()
|
||||
restoresGlob := filepath.Join(restoresDir, "*.json")
|
||||
restoresJson, err := findFiles(restoresGlob, excludeFiles)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to find velero restores files under %s", restoresDir)
|
||||
}
|
||||
restores := []*velerov1.Restore{}
|
||||
for key, restoreJson := range restoresJson {
|
||||
var restoreArray []*velerov1.Restore
|
||||
err := json.Unmarshal(restoreJson, &restoreArray)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to unmarshal restore json from %s", key)
|
||||
}
|
||||
restores = append(restores, restoreArray...)
|
||||
}
|
||||
|
||||
// get schedules.velero.io
|
||||
schedulesDir := GetVeleroSchedulesDirectory()
|
||||
schedulesGlob := filepath.Join(schedulesDir, "*.json")
|
||||
schedulesJson, err := findFiles(schedulesGlob, excludeFiles)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to find velero schedules files under %s", schedulesDir)
|
||||
}
|
||||
schedules := []*velerov1.Schedule{}
|
||||
for key, scheduleJson := range schedulesJson {
|
||||
var scheduleArray []*velerov1.Schedule
|
||||
err := json.Unmarshal(scheduleJson, &scheduleArray)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to unmarshal schedule json from %s", key)
|
||||
}
|
||||
schedules = append(schedules, scheduleArray...)
|
||||
}
|
||||
|
||||
// get serverstatusrequests.velero.io
|
||||
serverStatusRequestsDir := GetVeleroServerStatusRequestsDirectory()
|
||||
serverStatusRequestsGlob := filepath.Join(serverStatusRequestsDir, "*.json")
|
||||
serverStatusRequestsJson, err := findFiles(serverStatusRequestsGlob, excludeFiles)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to find velero server status requests files under %s", serverStatusRequestsDir)
|
||||
}
|
||||
serverStatusRequests := []*velerov1.ServerStatusRequest{}
|
||||
for key, serverStatusRequestJson := range serverStatusRequestsJson {
|
||||
var serverStatusRequestArray []*velerov1.ServerStatusRequest
|
||||
err := json.Unmarshal(serverStatusRequestJson, &serverStatusRequestArray)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to unmarshal server status request json from %s", key)
|
||||
}
|
||||
serverStatusRequests = append(serverStatusRequests, serverStatusRequestArray...)
|
||||
}
|
||||
|
||||
// get volumesnapshotlocations.velero.io
|
||||
volumeSnapshotLocationsDir := GetVeleroVolumeSnapshotLocationsDirectory()
|
||||
volumeSnapshotLocationsGlob := filepath.Join(volumeSnapshotLocationsDir, "*.json")
|
||||
volumeSnapshotLocationsJson, err := findFiles(volumeSnapshotLocationsGlob, excludeFiles)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to find velero volume snapshot locations files under %s", volumeSnapshotLocationsDir)
|
||||
}
|
||||
volumeSnapshotLocations := []*velerov1.VolumeSnapshotLocation{}
|
||||
for key, volumeSnapshotLocationJson := range volumeSnapshotLocationsJson {
|
||||
var volumeSnapshotLocationArray []*velerov1.VolumeSnapshotLocation
|
||||
err := json.Unmarshal(volumeSnapshotLocationJson, &volumeSnapshotLocationArray)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to unmarshal volume snapshot location json from %s", key)
|
||||
}
|
||||
volumeSnapshotLocations = append(volumeSnapshotLocations, volumeSnapshotLocationArray...)
|
||||
}
|
||||
|
||||
logsDir := GetVeleroLogsDirectory()
|
||||
logsGlob := filepath.Join(logsDir, "node-agent*", "*.log")
|
||||
nodeAgentlogs, err := findFiles(logsGlob, excludeFiles)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to find velero logs files under %s", logsDir)
|
||||
}
|
||||
|
||||
veleroLogsGlob := filepath.Join(logsDir, "velero*", "*.log")
|
||||
veleroLogs, err := findFiles(veleroLogsGlob, excludeFiles)
|
||||
|
||||
results = append(results, analyzeLogs(nodeAgentlogs, "node-agent*")...)
|
||||
results = append(results, analyzeLogs(veleroLogs, "velero*")...)
|
||||
results = append(results, analyzeBackups(backups)...)
|
||||
results = append(results, analyzeBackupStorageLocations(backupStorageLocations)...)
|
||||
results = append(results, analyzeDeleteBackupRequests(deleteBackupRequests)...)
|
||||
results = append(results, analyzePodVolumeBackups(podVolumeBackups)...)
|
||||
results = append(results, analyzePodVolumeRestores(podVolumeRestores)...)
|
||||
results = append(results, analyzeRestores(restores)...)
|
||||
results = append(results, analyzeSchedules(schedules)...)
|
||||
results = append(results, analyzeVolumeSnapshotLocations(volumeSnapshotLocations)...)
|
||||
|
||||
return aggregateResults(results), nil
|
||||
}
|
||||
|
||||
func analyzeBackupRepositories(backupRepositories []*velerov1.BackupRepository) []*AnalyzeResult {
|
||||
results := []*AnalyzeResult{}
|
||||
readyCount := 0
|
||||
backupRepositoriesResult := &AnalyzeResult{
|
||||
Title: "At least 1 Backup Repository configured",
|
||||
}
|
||||
if len(backupRepositories) == 0 {
|
||||
backupRepositoriesResult.IsFail = true
|
||||
backupRepositoriesResult.Message = "No backup repositories configured"
|
||||
} else {
|
||||
for _, backupRepository := range backupRepositories {
|
||||
if backupRepository.Status.Phase != velerov1.BackupRepositoryPhaseReady {
|
||||
result := &AnalyzeResult{
|
||||
Title: fmt.Sprintf("Backup Repository %s", backupRepository.Name),
|
||||
}
|
||||
result.Message = fmt.Sprintf("Backup Repository [%s] is in phase %s", backupRepository.Name, backupRepository.Status.Phase)
|
||||
result.IsWarn = true
|
||||
results = append(results, result)
|
||||
} else {
|
||||
readyCount++
|
||||
}
|
||||
}
|
||||
if readyCount > 0 {
|
||||
backupRepositoriesResult.IsPass = true
|
||||
backupRepositoriesResult.Message = fmt.Sprintf("Found %d backup repositories configured and %d Ready", len(backupRepositories), readyCount)
|
||||
} else {
|
||||
backupRepositoriesResult.IsWarn = true
|
||||
backupRepositoriesResult.Message = fmt.Sprintf("Found %d configured backup repositories, but none are ready", len(backupRepositories))
|
||||
}
|
||||
}
|
||||
results = append(results, backupRepositoriesResult)
|
||||
return results
|
||||
|
||||
}
|
||||
|
||||
func analyzeResticRepositories(resticRepositories []*restic_types.ResticRepository) []*AnalyzeResult {
|
||||
results := []*AnalyzeResult{}
|
||||
readyCount := 0
|
||||
resticRepositoriesResult := &AnalyzeResult{
|
||||
Title: "At least 1 Restic Repository configured",
|
||||
}
|
||||
if len(resticRepositories) == 0 {
|
||||
resticRepositoriesResult.IsFail = true
|
||||
resticRepositoriesResult.Message = "No restic repositories configured"
|
||||
} else {
|
||||
for _, resticRepository := range resticRepositories {
|
||||
if resticRepository.Status.Phase != restic_types.ResticRepositoryPhaseReady {
|
||||
result := &AnalyzeResult{
|
||||
Title: fmt.Sprintf("Restic Repository %s", resticRepository.GetName()),
|
||||
}
|
||||
result.Message = fmt.Sprintf("Restic Repository [%s] is in phase %s", resticRepository.Name, resticRepository.Status.Phase)
|
||||
result.IsWarn = true
|
||||
results = append(results, result)
|
||||
} else {
|
||||
readyCount++
|
||||
}
|
||||
}
|
||||
if readyCount > 0 {
|
||||
resticRepositoriesResult.IsPass = true
|
||||
resticRepositoriesResult.Message = fmt.Sprintf("Found %d restic repositories configured and %d Ready", len(resticRepositories), readyCount)
|
||||
} else {
|
||||
resticRepositoriesResult.IsWarn = true
|
||||
resticRepositoriesResult.Message = fmt.Sprintf("Found %d configured restic repositories, but none are ready", len(resticRepositories))
|
||||
}
|
||||
}
|
||||
results = append(results, resticRepositoriesResult)
|
||||
return results
|
||||
}
|
||||
|
||||
func analyzeBackups(backups []*velerov1.Backup) []*AnalyzeResult {
|
||||
results := []*AnalyzeResult{}
|
||||
|
||||
failedPhases := map[velerov1.BackupPhase]bool{
|
||||
velerov1.BackupPhaseFailed: true,
|
||||
velerov1.BackupPhasePartiallyFailed: true,
|
||||
velerov1.BackupPhaseFailedValidation: true,
|
||||
velerov1.BackupPhaseFinalizingPartiallyFailed: true,
|
||||
velerov1.BackupPhaseWaitingForPluginOperationsPartiallyFailed: true,
|
||||
}
|
||||
|
||||
for _, backup := range backups {
|
||||
|
||||
if failedPhases[backup.Status.Phase] {
|
||||
result := &AnalyzeResult{
|
||||
Title: fmt.Sprintf("Backup %s", backup.Name),
|
||||
}
|
||||
result.IsFail = true
|
||||
result.Message = fmt.Sprintf("Backup %s phase is %s", backup.Name, backup.Status.Phase)
|
||||
results = append(results, result)
|
||||
|
||||
}
|
||||
}
|
||||
if len(backups) > 0 {
|
||||
results = append(results, &AnalyzeResult{
|
||||
Title: "Velero Backups",
|
||||
IsPass: true,
|
||||
Message: fmt.Sprintf("Found %d backups", len(backups)),
|
||||
})
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func analyzeBackupStorageLocations(backupStorageLocations []*velerov1.BackupStorageLocation) []*AnalyzeResult {
|
||||
results := []*AnalyzeResult{}
|
||||
availableCount := 0
|
||||
bslResult := &AnalyzeResult{
|
||||
Title: "At least 1 Backup Storage Location configured",
|
||||
}
|
||||
|
||||
if len(backupStorageLocations) == 0 {
|
||||
bslResult.IsFail = true
|
||||
bslResult.Message = "No backup storage locations configured"
|
||||
} else {
|
||||
for _, backupStorageLocation := range backupStorageLocations {
|
||||
if backupStorageLocation.Status.Phase != velerov1.BackupStorageLocationPhaseAvailable {
|
||||
result := &AnalyzeResult{
|
||||
Title: fmt.Sprintf("Backup Storage Location %s", backupStorageLocation.Name),
|
||||
}
|
||||
result.Message = fmt.Sprintf("Backup Storage Location [%s] is in phase %s", backupStorageLocation.Name, backupStorageLocation.Status.Phase)
|
||||
result.IsWarn = true
|
||||
results = append(results, result)
|
||||
} else {
|
||||
availableCount++
|
||||
}
|
||||
}
|
||||
if availableCount > 0 {
|
||||
bslResult.IsPass = true
|
||||
bslResult.Message = fmt.Sprintf("Found %d backup storage locations configured and %d Available", len(backupStorageLocations), availableCount)
|
||||
} else {
|
||||
bslResult.IsWarn = true
|
||||
bslResult.Message = fmt.Sprintf("Found %d configured backup storage locations, but none are available", len(backupStorageLocations))
|
||||
}
|
||||
}
|
||||
results = append(results, bslResult)
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func analyzeDeleteBackupRequests(deleteBackupRequests []*velerov1.DeleteBackupRequest) []*AnalyzeResult {
|
||||
results := []*AnalyzeResult{}
|
||||
inProgressCount := 0
|
||||
if len(deleteBackupRequests) > 0 {
|
||||
for _, deleteBackupRequest := range deleteBackupRequests {
|
||||
if deleteBackupRequest.Status.Phase == velerov1.DeleteBackupRequestPhaseInProgress {
|
||||
inProgressCount++
|
||||
}
|
||||
}
|
||||
if inProgressCount > 0 {
|
||||
deleteBackupRequestsResult := &AnalyzeResult{
|
||||
Title: "Delete Backup Requests summary",
|
||||
}
|
||||
deleteBackupRequestsResult.IsWarn = true
|
||||
deleteBackupRequestsResult.Message = fmt.Sprintf("Found %d delete backup requests in progress", inProgressCount)
|
||||
results = append(results, deleteBackupRequestsResult)
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func analyzePodVolumeBackups(podVolumeBackups []*velerov1.PodVolumeBackup) []*AnalyzeResult {
|
||||
results := []*AnalyzeResult{}
|
||||
failures := 0
|
||||
if len(podVolumeBackups) > 0 {
|
||||
for _, podVolumeBackup := range podVolumeBackups {
|
||||
if podVolumeBackup.Status.Phase == velerov1.PodVolumeBackupPhaseFailed {
|
||||
result := &AnalyzeResult{
|
||||
Title: fmt.Sprintf("Pod Volume Backup %s", podVolumeBackup.Name),
|
||||
}
|
||||
result.IsFail = true
|
||||
result.Message = fmt.Sprintf("Pod Volume Backup %s phase is %s", podVolumeBackup.Name, podVolumeBackup.Status.Phase)
|
||||
results = append(results, result)
|
||||
failures++
|
||||
}
|
||||
}
|
||||
|
||||
if failures == 0 {
|
||||
results = append(results, &AnalyzeResult{
|
||||
Title: "Pod Volume Backups",
|
||||
IsPass: true,
|
||||
Message: fmt.Sprintf("Found %d pod volume backups", len(podVolumeBackups)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func analyzePodVolumeRestores(podVolumeRestores []*velerov1.PodVolumeRestore) []*AnalyzeResult {
|
||||
results := []*AnalyzeResult{}
|
||||
failures := 0
|
||||
|
||||
if len(podVolumeRestores) > 0 {
|
||||
for _, podVolumeRestore := range podVolumeRestores {
|
||||
if podVolumeRestore.Status.Phase == velerov1.PodVolumeRestorePhaseFailed {
|
||||
result := &AnalyzeResult{
|
||||
Title: fmt.Sprintf("Pod Volume Restore %s", podVolumeRestore.Name),
|
||||
}
|
||||
result.IsFail = true
|
||||
result.Message = fmt.Sprintf("Pod Volume Restore %s phase is %s", podVolumeRestore.Name, podVolumeRestore.Status.Phase)
|
||||
results = append(results, result)
|
||||
failures++
|
||||
}
|
||||
}
|
||||
if failures == 0 {
|
||||
results = append(results, &AnalyzeResult{
|
||||
Title: "Pod Volume Restores",
|
||||
IsPass: true,
|
||||
Message: fmt.Sprintf("Found %d pod volume restores", len(podVolumeRestores)),
|
||||
})
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func analyzeRestores(restores []*velerov1.Restore) []*AnalyzeResult {
|
||||
results := []*AnalyzeResult{}
|
||||
failures := 0
|
||||
|
||||
if len(restores) > 0 {
|
||||
|
||||
failedPhases := map[velerov1.RestorePhase]bool{
|
||||
velerov1.RestorePhaseFailed: true,
|
||||
velerov1.RestorePhasePartiallyFailed: true,
|
||||
velerov1.RestorePhaseFailedValidation: true,
|
||||
velerov1.RestorePhaseWaitingForPluginOperationsPartiallyFailed: true,
|
||||
}
|
||||
|
||||
for _, restore := range restores {
|
||||
if failedPhases[restore.Status.Phase] {
|
||||
result := &AnalyzeResult{
|
||||
Title: fmt.Sprintf("Restore %s", restore.Name),
|
||||
}
|
||||
result.IsFail = true
|
||||
result.Message = fmt.Sprintf("Restore %s phase is %s", restore.Name, restore.Status.Phase)
|
||||
results = append(results, result)
|
||||
failures++
|
||||
}
|
||||
}
|
||||
if failures == 0 {
|
||||
results = append(results, &AnalyzeResult{
|
||||
Title: "Velero Restores",
|
||||
IsPass: true,
|
||||
Message: fmt.Sprintf("Found %d restores", len(restores)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func analyzeSchedules(schedules []*velerov1.Schedule) []*AnalyzeResult {
|
||||
results := []*AnalyzeResult{}
|
||||
failures := 0
|
||||
if len(schedules) > 0 {
|
||||
for _, schedule := range schedules {
|
||||
if schedule.Status.Phase == velerov1.SchedulePhaseFailedValidation {
|
||||
result := &AnalyzeResult{
|
||||
Title: fmt.Sprintf("Schedule %s", schedule.Name),
|
||||
}
|
||||
result.IsFail = true
|
||||
result.Message = fmt.Sprintf("Schedule %s phase is %s", schedule.Name, schedule.Status.Phase)
|
||||
results = append(results, result)
|
||||
failures++
|
||||
}
|
||||
}
|
||||
if failures == 0 {
|
||||
results = append(results, &AnalyzeResult{
|
||||
Title: "Velero Schedules",
|
||||
IsPass: true,
|
||||
Message: fmt.Sprintf("Found %d schedules", len(schedules)),
|
||||
})
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func analyzeVolumeSnapshotLocations(volumeSnapshotLocations []*velerov1.VolumeSnapshotLocation) []*AnalyzeResult {
|
||||
results := []*AnalyzeResult{}
|
||||
failures := 0
|
||||
if len(volumeSnapshotLocations) > 0 {
|
||||
for _, volumeSnapshotLocation := range volumeSnapshotLocations {
|
||||
if volumeSnapshotLocation.Status.Phase == velerov1.VolumeSnapshotLocationPhaseUnavailable {
|
||||
result := &AnalyzeResult{
|
||||
Title: fmt.Sprintf("Volume Snapshot Location %s", volumeSnapshotLocation.Name),
|
||||
}
|
||||
result.IsFail = true
|
||||
result.Message = fmt.Sprintf("Volume Snapshot Location %s phase is %s", volumeSnapshotLocation.Name, volumeSnapshotLocation.Status.Phase)
|
||||
results = append(results, result)
|
||||
failures++
|
||||
}
|
||||
}
|
||||
if failures == 0 {
|
||||
results = append(results, &AnalyzeResult{
|
||||
Title: "Velero Volume Snapshot Locations",
|
||||
IsPass: true,
|
||||
Message: fmt.Sprintf("Found %d volume snapshot locations", len(volumeSnapshotLocations)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func analyzeLogs(logs map[string][]byte, kind string) []*AnalyzeResult {
|
||||
results := []*AnalyzeResult{}
|
||||
if len(logs) > 0 {
|
||||
for key, logBytes := range logs {
|
||||
logContent := string(logBytes)
|
||||
result := &AnalyzeResult{
|
||||
Title: fmt.Sprintf("Velero logs for pod [%s]", key),
|
||||
}
|
||||
if strings.Contains(logContent, "permission denied") {
|
||||
result.IsWarn = true
|
||||
result.Message = fmt.Sprintf("Found 'permission denied' in %s pod log file(s)", kind)
|
||||
results = append(results, result)
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.Contains(logContent, "error") || strings.Contains(logContent, "panic") || strings.Contains(logContent, "fatal") {
|
||||
result.IsWarn = true
|
||||
result.Message = fmt.Sprintf("Found error|panic|fatal in %s pod log file(s)", kind)
|
||||
results = append(results, result)
|
||||
}
|
||||
}
|
||||
|
||||
results = append(results, &AnalyzeResult{
|
||||
Title: fmt.Sprintf("Velero Logs analysis for kind [%s]", kind),
|
||||
IsPass: true,
|
||||
Message: fmt.Sprintf("Found %d log files", len(logs)),
|
||||
})
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func aggregateResults(results []*AnalyzeResult) []*AnalyzeResult {
|
||||
out := []*AnalyzeResult{}
|
||||
resultFailed := false
|
||||
for _, result := range results {
|
||||
if result.IsFail {
|
||||
resultFailed = true
|
||||
}
|
||||
out = append(out, result)
|
||||
}
|
||||
if len(results) > 0 {
|
||||
if resultFailed == false {
|
||||
out = append(out, &AnalyzeResult{
|
||||
Title: "Velero Status",
|
||||
IsPass: true,
|
||||
Message: "Velero setup is healthy",
|
||||
})
|
||||
}
|
||||
if resultFailed == true {
|
||||
out = append(out, &AnalyzeResult{
|
||||
Title: "Velero Status",
|
||||
IsWarn: true,
|
||||
Message: "Velero setup is not entirely healthy",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func getVeleroVersion(excludedFiles []string, findFiles getChildCollectedFileContents) (string, error) {
|
||||
veleroDeploymentDir := "cluster-resources/deployments"
|
||||
veleroVersion := ""
|
||||
veleroDeploymentGlob := filepath.Join(veleroDeploymentDir, "velero.json")
|
||||
veleroDeploymentJson, err := findFiles(veleroDeploymentGlob, excludedFiles)
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "failed to find velero deployment file under %s", veleroDeploymentDir)
|
||||
}
|
||||
var deploymentList *appsV1.DeploymentList
|
||||
// should run only once
|
||||
for key, veleroDeploymentJsonBytes := range veleroDeploymentJson {
|
||||
err := json.Unmarshal(veleroDeploymentJsonBytes, &deploymentList)
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "failed to unmarshal velero deployment json from %s", key)
|
||||
}
|
||||
break
|
||||
}
|
||||
for _, deployment := range deploymentList.Items {
|
||||
for _, container := range deployment.Spec.Template.Spec.Containers {
|
||||
if container.Name == "velero" {
|
||||
container_image := container.Image
|
||||
veleroVersion = strings.Split(container_image, ":")[1]
|
||||
return veleroVersion, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", errors.Errorf("Unable to get velero version. Could not find velero container in deployment!")
|
||||
}
|
||||
|
||||
func GetVeleroBackupsDirectory() string {
|
||||
return "cluster-resources/custom-resources/backups.velero.io"
|
||||
}
|
||||
|
||||
func GetVeleroBackupRepositoriesDirectory() string {
|
||||
return "cluster-resources/custom-resources/backuprepositories.velero.io"
|
||||
}
|
||||
|
||||
func GetVeleroBackupStorageLocationsDirectory() string {
|
||||
return "cluster-resources/custom-resources/backupstoragelocations.velero.io"
|
||||
}
|
||||
|
||||
func GetVeleroDeleteBackupRequestsDirectory() string {
|
||||
return "cluster-resources/custom-resources/deletebackuprequests.velero.io"
|
||||
}
|
||||
|
||||
func GetVeleroDownloadRequestsDirectory() string {
|
||||
return "cluster-resources/custom-resources/downloadrequests.velero.io"
|
||||
}
|
||||
|
||||
func GetVeleroLogsDirectory() string {
|
||||
return "velero/logs"
|
||||
}
|
||||
|
||||
func GetVeleroPodVolumeBackupsDirectory() string {
|
||||
return "cluster-resources/custom-resources/podvolumebackups.velero.io"
|
||||
}
|
||||
|
||||
func GetVeleroPodVolumeRestoresDirectory() string {
|
||||
return "cluster-resources/custom-resources/podvolumerestores.velero.io"
|
||||
}
|
||||
|
||||
func GetVeleroRestoresDirectory() string {
|
||||
return "cluster-resources/custom-resources/restores.velero.io"
|
||||
}
|
||||
|
||||
func GetVeleroSchedulesDirectory() string {
|
||||
return "cluster-resources/custom-resources/schedules.velero.io"
|
||||
}
|
||||
|
||||
func GetVeleroServerStatusRequestsDirectory() string {
|
||||
return "cluster-resources/custom-resources/serverstatusrequests.velero.io"
|
||||
}
|
||||
|
||||
func GetVeleroVolumeSnapshotLocationsDirectory() string {
|
||||
return "cluster-resources/custom-resources/volumesnapshotlocations.velero.io"
|
||||
}
|
||||
|
||||
func GetVeleroResticRepositoriesDirectory() string {
|
||||
return "cluster-resources/custom-resources/resticrepositories.velero.io"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,7 @@ package v1beta1
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/replicatedhq/troubleshoot/pkg/multitype"
|
||||
authorizationv1 "k8s.io/api/authorization/v1"
|
||||
@@ -100,6 +101,7 @@ type Get struct {
|
||||
URL string `json:"url" yaml:"url"`
|
||||
InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty" yaml:"insecureSkipVerify,omitempty"`
|
||||
Headers map[string]string `json:"headers,omitempty" yaml:"headers,omitempty"`
|
||||
Timeout time.Duration `json:"timeout,omitempty" yaml:"timeout,omitempty"`
|
||||
}
|
||||
|
||||
type Post struct {
|
||||
@@ -107,6 +109,7 @@ type Post struct {
|
||||
InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty" yaml:"insecureSkipVerify,omitempty"`
|
||||
Headers map[string]string `json:"headers,omitempty" yaml:"headers,omitempty"`
|
||||
Body string `json:"body,omitempty" yaml:"body,omitempty"`
|
||||
Timeout time.Duration `json:"timeout,omitempty" yaml:"timeout,omitempty"`
|
||||
}
|
||||
|
||||
type Put struct {
|
||||
@@ -114,6 +117,7 @@ type Put struct {
|
||||
InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty" yaml:"insecureSkipVerify,omitempty"`
|
||||
Headers map[string]string `json:"headers,omitempty" yaml:"headers,omitempty"`
|
||||
Body string `json:"body,omitempty" yaml:"body,omitempty"`
|
||||
Timeout time.Duration `json:"timeout,omitempty" yaml:"timeout,omitempty"`
|
||||
}
|
||||
|
||||
type Database struct {
|
||||
|
||||
@@ -186,6 +186,10 @@ type CephStatusAnalyze struct {
|
||||
Namespace string `json:"namespace" yaml:"namespace"`
|
||||
}
|
||||
|
||||
type VeleroAnalyze struct {
|
||||
AnalyzeMeta `json:",inline" yaml:",inline"`
|
||||
}
|
||||
|
||||
type LonghornAnalyze struct {
|
||||
AnalyzeMeta `json:",inline" yaml:",inline"`
|
||||
Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"`
|
||||
@@ -245,6 +249,7 @@ type Analyze struct {
|
||||
Mysql *DatabaseAnalyze `json:"mysql,omitempty" yaml:"mysql,omitempty"`
|
||||
Redis *DatabaseAnalyze `json:"redis,omitempty" yaml:"redis,omitempty"`
|
||||
CephStatus *CephStatusAnalyze `json:"cephStatus,omitempty" yaml:"cephStatus,omitempty"`
|
||||
Velero *VeleroAnalyze `json:"velero,omitempty" yaml:"velero,omitempty"`
|
||||
Longhorn *LonghornAnalyze `json:"longhorn,omitempty" yaml:"longhorn,omitempty"`
|
||||
RegistryImages *RegistryImagesAnalyze `json:"registryImages,omitempty" yaml:"registryImages,omitempty"`
|
||||
WeaveReport *WeaveReportAnalyze `json:"weaveReport,omitempty" yaml:"weaveReport,omitempty"`
|
||||
|
||||
@@ -168,6 +168,9 @@ type Get struct {
|
||||
URL string `json:"url" yaml:"url"`
|
||||
InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty" yaml:"insecureSkipVerify,omitempty"`
|
||||
Headers map[string]string `json:"headers,omitempty" yaml:"headers,omitempty"`
|
||||
// Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m.
|
||||
// Missing value or empty string or means no timeout.
|
||||
Timeout string `json:"timeout,omitempty" yaml:"timeout,omitempty"`
|
||||
}
|
||||
|
||||
type Post struct {
|
||||
@@ -175,6 +178,9 @@ type Post struct {
|
||||
InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty" yaml:"insecureSkipVerify,omitempty"`
|
||||
Headers map[string]string `json:"headers,omitempty" yaml:"headers,omitempty"`
|
||||
Body string `json:"body,omitempty" yaml:"body,omitempty"`
|
||||
// Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m.
|
||||
// Missing value or empty string or means no timeout.
|
||||
Timeout string `json:"timeout,omitempty" yaml:"timeout,omitempty"`
|
||||
}
|
||||
|
||||
type Put struct {
|
||||
@@ -182,6 +188,9 @@ type Put struct {
|
||||
InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty" yaml:"insecureSkipVerify,omitempty"`
|
||||
Headers map[string]string `json:"headers,omitempty" yaml:"headers,omitempty"`
|
||||
Body string `json:"body,omitempty" yaml:"body,omitempty"`
|
||||
// Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m.
|
||||
// Missing value or empty string or means no timeout.
|
||||
Timeout string `json:"timeout,omitempty" yaml:"timeout,omitempty"`
|
||||
}
|
||||
|
||||
type Database struct {
|
||||
@@ -244,6 +253,12 @@ type CertificateSource struct {
|
||||
Namespaces []string `json:"namespaces,omitempty" yaml:"namespaces,omitempty"`
|
||||
}
|
||||
|
||||
type Helm struct {
|
||||
CollectorMeta `json:",inline" yaml:",inline"`
|
||||
Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"`
|
||||
ReleaseName string `json:"releaseName,omitempty" yaml:"releaseName,omitempty"`
|
||||
}
|
||||
|
||||
type Collect struct {
|
||||
ClusterInfo *ClusterInfo `json:"clusterInfo,omitempty" yaml:"clusterInfo,omitempty"`
|
||||
ClusterResources *ClusterResources `json:"clusterResources,omitempty" yaml:"clusterResources,omitempty"`
|
||||
@@ -268,6 +283,7 @@ type Collect struct {
|
||||
RegistryImages *RegistryImages `json:"registryImages,omitempty" yaml:"registryImages,omitempty"`
|
||||
Sysctl *Sysctl `json:"sysctl,omitempty" yaml:"sysctl,omitempty"`
|
||||
Certificates *Certificates `json:"certificates,omitempty" yaml:"certificates,omitempty"`
|
||||
Helm *Helm `json:"helm,omitempty" yaml:"helm,omitempty"`
|
||||
}
|
||||
|
||||
func (c *Collect) AccessReviewSpecs(overrideNS string) []authorizationv1.SelfSubjectAccessReviewSpec {
|
||||
|
||||
@@ -180,8 +180,13 @@ type HostServices struct {
|
||||
|
||||
type HostRun struct {
|
||||
HostCollectorMeta `json:",inline" yaml:",inline"`
|
||||
Command string `json:"command"`
|
||||
Args []string `json:"args"`
|
||||
Command string `json:"command"`
|
||||
Args []string `json:"args"`
|
||||
OutputDir string `json:"outputDir,omitempty" yaml:"outputDir,omitempty"`
|
||||
Input map[string]string `json:"input,omitempty" yaml:"input,omitempty"`
|
||||
Env []string `json:"env,omitempty" yaml:"env,omitempty"`
|
||||
InheritEnvs []string `json:"inheritEnvs,omitempty" yaml:"inheritEnvs,omitempty"`
|
||||
IgnoreParentEnvs bool `json:"ignoreParentEnvs,omitempty" yaml:"ignoreParentEnvs,omitempty"`
|
||||
}
|
||||
|
||||
type HostCollect struct {
|
||||
|
||||
@@ -169,6 +169,11 @@ func (in *Analyze) DeepCopyInto(out *Analyze) {
|
||||
*out = new(CephStatusAnalyze)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.Velero != nil {
|
||||
in, out := &in.Velero, &out.Velero
|
||||
*out = new(VeleroAnalyze)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.Longhorn != nil {
|
||||
in, out := &in.Longhorn, &out.Longhorn
|
||||
*out = new(LonghornAnalyze)
|
||||
@@ -882,6 +887,11 @@ func (in *Collect) DeepCopyInto(out *Collect) {
|
||||
*out = new(Certificates)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.Helm != nil {
|
||||
in, out := &in.Helm, &out.Helm
|
||||
*out = new(Helm)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Collect.
|
||||
@@ -1599,6 +1609,22 @@ func (in *HTTPLoadBalancerAnalyze) DeepCopy() *HTTPLoadBalancerAnalyze {
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Helm) DeepCopyInto(out *Helm) {
|
||||
*out = *in
|
||||
in.CollectorMeta.DeepCopyInto(&out.CollectorMeta)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Helm.
|
||||
func (in *Helm) DeepCopy() *Helm {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Helm)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *HostAnalyze) DeepCopyInto(out *HostAnalyze) {
|
||||
*out = *in
|
||||
@@ -2281,6 +2307,23 @@ func (in *HostRun) DeepCopyInto(out *HostRun) {
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.Input != nil {
|
||||
in, out := &in.Input, &out.Input
|
||||
*out = make(map[string]string, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
if in.Env != nil {
|
||||
in, out := &in.Env, &out.Env
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.InheritEnvs != nil {
|
||||
in, out := &in.InheritEnvs, &out.InheritEnvs
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostRun.
|
||||
@@ -4633,6 +4676,22 @@ func (in *UDPPortStatusAnalyze) DeepCopy() *UDPPortStatusAnalyze {
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *VeleroAnalyze) DeepCopyInto(out *VeleroAnalyze) {
|
||||
*out = *in
|
||||
in.AnalyzeMeta.DeepCopyInto(&out.AnalyzeMeta)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VeleroAnalyze.
|
||||
func (in *VeleroAnalyze) DeepCopy() *VeleroAnalyze {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(VeleroAnalyze)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *WeaveReportAnalyze) DeepCopyInto(out *WeaveReportAnalyze) {
|
||||
*out = *in
|
||||
|
||||
@@ -98,6 +98,13 @@ var CephCommands = []CephCommand{
|
||||
Format: "txt",
|
||||
DefaultTimeout: "30s",
|
||||
},
|
||||
{
|
||||
ID: "df", // the disk usage of each pool
|
||||
Command: []string{"ceph", "df"},
|
||||
Args: []string{"-f", "json-pretty"},
|
||||
Format: "json",
|
||||
DefaultTimeout: "30s",
|
||||
},
|
||||
{
|
||||
ID: "osd-df",
|
||||
Command: []string{"ceph", "osd", "df"},
|
||||
|
||||
@@ -120,19 +120,16 @@ func (c *CollectClusterResources) Collect(progressChan chan<- interface{}) (Coll
|
||||
var namespaceNames []string
|
||||
if len(c.Collector.Namespaces) > 0 {
|
||||
namespaces, namespaceErrors := getNamespaces(ctx, client, c.Collector.Namespaces)
|
||||
klog.V(4).Infof("checking for namespaces access: %s", string(namespaces))
|
||||
namespaceNames = c.Collector.Namespaces
|
||||
output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_NAMESPACES)), bytes.NewBuffer(namespaces))
|
||||
output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_NAMESPACES)), marshalErrors(namespaceErrors))
|
||||
} else if c.Namespace != "" {
|
||||
namespace, namespaceErrors := getNamespace(ctx, client, c.Namespace)
|
||||
klog.V(4).Infof("checking for namespace access: %s", string(namespace))
|
||||
output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_NAMESPACES)), bytes.NewBuffer(namespace))
|
||||
output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_NAMESPACES)), marshalErrors(namespaceErrors))
|
||||
namespaceNames = append(namespaceNames, c.Namespace)
|
||||
} else {
|
||||
namespaces, namespaceList, namespaceErrors := getAllNamespaces(ctx, client)
|
||||
klog.V(4).Infof("checking for all namespaces access: %s", string(namespaces))
|
||||
output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_NAMESPACES)), bytes.NewBuffer(namespaces))
|
||||
output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_NAMESPACES)), marshalErrors(namespaceErrors))
|
||||
if namespaceList != nil {
|
||||
@@ -146,6 +143,7 @@ func (c *CollectClusterResources) Collect(progressChan chan<- interface{}) (Coll
|
||||
reviewStatuses, reviewStatusErrors := getSelfSubjectRulesReviews(ctx, client, namespaceNames)
|
||||
|
||||
// auth cani
|
||||
klog.V(2).Infof("checking [%s] namespaces for permissions to collect resources", strings.Join(namespaceNames, ", "))
|
||||
authCanI := authCanI(reviewStatuses, namespaceNames)
|
||||
for k, v := range authCanI {
|
||||
output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_AUTH_CANI, k), bytes.NewBuffer(v))
|
||||
@@ -160,8 +158,12 @@ func (c *CollectClusterResources) Collect(progressChan chan<- interface{}) (Coll
|
||||
filteredNamespaces = append(filteredNamespaces, ns)
|
||||
}
|
||||
}
|
||||
if len(filteredNamespaces) != len(namespaceNames) {
|
||||
klog.V(2).Infof("filtered namespaces down to [%s] after evaluating permissions", strings.Join(filteredNamespaces, ", "))
|
||||
} else {
|
||||
klog.V(2).Infof("no namespaces filtered out after evaluating permissions")
|
||||
}
|
||||
namespaceNames = filteredNamespaces
|
||||
klog.V(4).Infof("filtered to namespaceNames %s", namespaceNames)
|
||||
}
|
||||
|
||||
// pods
|
||||
@@ -1235,15 +1237,26 @@ func crdsV1beta(ctx context.Context, config *rest.Config) ([]byte, []string) {
|
||||
}
|
||||
|
||||
func crs(ctx context.Context, dyn dynamic.Interface, client *kubernetes.Clientset, config *rest.Config, namespaces []string) (map[string][]byte, map[string]string) {
|
||||
errorList := make(map[string]string)
|
||||
ok, err := discovery.HasResource(client, "apiextensions.k8s.io/v1", "CustomResourceDefinition")
|
||||
if err != nil {
|
||||
return nil, map[string]string{"discover apiextensions.k8s.io/v1": err.Error()}
|
||||
}
|
||||
if ok {
|
||||
return crsV1(ctx, dyn, config, namespaces)
|
||||
crdClient, err := apiextensionsv1clientset.NewForConfig(config)
|
||||
if err != nil {
|
||||
errorList["crdClient"] = err.Error()
|
||||
return map[string][]byte{}, errorList
|
||||
}
|
||||
return crsV1(ctx, dyn, crdClient, namespaces)
|
||||
}
|
||||
|
||||
return crsV1beta(ctx, dyn, config, namespaces)
|
||||
crdClient, err := apiextensionsv1beta1clientset.NewForConfig(config)
|
||||
if err != nil {
|
||||
errorList["crdClient"] = err.Error()
|
||||
return map[string][]byte{}, errorList
|
||||
}
|
||||
return crsV1beta(ctx, dyn, crdClient, namespaces)
|
||||
}
|
||||
|
||||
// Selects the newest version by kube-aware priority.
|
||||
@@ -1258,16 +1271,15 @@ func selectCRDVersionByPriority(versions []string) string {
|
||||
return versions[len(versions)-1]
|
||||
}
|
||||
|
||||
func crsV1(ctx context.Context, client dynamic.Interface, config *rest.Config, namespaces []string) (map[string][]byte, map[string]string) {
|
||||
func crsV1(
|
||||
ctx context.Context,
|
||||
client dynamic.Interface,
|
||||
crdClient apiextensionsv1clientset.ApiextensionsV1Interface,
|
||||
namespaces []string,
|
||||
) (map[string][]byte, map[string]string) {
|
||||
customResources := make(map[string][]byte)
|
||||
errorList := make(map[string]string)
|
||||
|
||||
crdClient, err := apiextensionsv1clientset.NewForConfig(config)
|
||||
if err != nil {
|
||||
errorList["crdClient"] = err.Error()
|
||||
return customResources, errorList
|
||||
}
|
||||
|
||||
crds, err := crdClient.CustomResourceDefinitions().List(ctx, metav1.ListOptions{})
|
||||
if err != nil {
|
||||
errorList["crdList"] = err.Error()
|
||||
@@ -1319,12 +1331,11 @@ func crsV1(ctx context.Context, client dynamic.Interface, config *rest.Config, n
|
||||
for _, item := range customResourceList.Items {
|
||||
objects = append(objects, item.Object)
|
||||
}
|
||||
b, err := yaml.Marshal(objects)
|
||||
err := storeCustomResource(crd.Name, objects, customResources)
|
||||
if err != nil {
|
||||
errorList[crd.Name] = err.Error()
|
||||
continue
|
||||
}
|
||||
customResources[fmt.Sprintf("%s.yaml", crd.Name)] = b
|
||||
} else {
|
||||
// Group fetched resources by the namespace
|
||||
perNamespace := map[string][]map[string]interface{}{}
|
||||
@@ -1353,13 +1364,11 @@ func crsV1(ctx context.Context, client dynamic.Interface, config *rest.Config, n
|
||||
}
|
||||
|
||||
namespacedName := fmt.Sprintf("%s/%s", crd.Name, ns)
|
||||
b, err := yaml.Marshal(perNamespace[ns])
|
||||
err := storeCustomResource(namespacedName, perNamespace[ns], customResources)
|
||||
if err != nil {
|
||||
errorList[namespacedName] = err.Error()
|
||||
continue
|
||||
}
|
||||
|
||||
customResources[fmt.Sprintf("%s.yaml", namespacedName)] = b
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1367,16 +1376,15 @@ func crsV1(ctx context.Context, client dynamic.Interface, config *rest.Config, n
|
||||
return customResources, errorList
|
||||
}
|
||||
|
||||
func crsV1beta(ctx context.Context, client dynamic.Interface, config *rest.Config, namespaces []string) (map[string][]byte, map[string]string) {
|
||||
func crsV1beta(
|
||||
ctx context.Context,
|
||||
client dynamic.Interface,
|
||||
crdClient apiextensionsv1beta1clientset.ApiextensionsV1beta1Interface,
|
||||
namespaces []string,
|
||||
) (map[string][]byte, map[string]string) {
|
||||
customResources := make(map[string][]byte)
|
||||
errorList := make(map[string]string)
|
||||
|
||||
crdClient, err := apiextensionsv1beta1clientset.NewForConfig(config)
|
||||
if err != nil {
|
||||
errorList["crdClient"] = err.Error()
|
||||
return customResources, errorList
|
||||
}
|
||||
|
||||
crds, err := crdClient.CustomResourceDefinitions().List(ctx, metav1.ListOptions{})
|
||||
if err != nil {
|
||||
errorList["crdList"] = err.Error()
|
||||
@@ -1430,12 +1438,13 @@ func crsV1beta(ctx context.Context, client dynamic.Interface, config *rest.Confi
|
||||
for _, item := range customResourceList.Items {
|
||||
objects = append(objects, item.Object)
|
||||
}
|
||||
b, err := yaml.Marshal(customResourceList.Items)
|
||||
|
||||
err = storeCustomResource(crd.Name, objects, customResources)
|
||||
if err != nil {
|
||||
errorList[crd.Name] = err.Error()
|
||||
continue
|
||||
}
|
||||
customResources[fmt.Sprintf("%s.yaml", crd.Name)] = b
|
||||
|
||||
} else {
|
||||
// Group fetched resources by the namespace
|
||||
perNamespace := map[string][]map[string]interface{}{}
|
||||
@@ -1464,13 +1473,11 @@ func crsV1beta(ctx context.Context, client dynamic.Interface, config *rest.Confi
|
||||
}
|
||||
|
||||
namespacedName := fmt.Sprintf("%s/%s", crd.Name, ns)
|
||||
b, err := yaml.Marshal(perNamespace[ns])
|
||||
err := storeCustomResource(namespacedName, perNamespace[ns], customResources)
|
||||
if err != nil {
|
||||
errorList[namespacedName] = err.Error()
|
||||
continue
|
||||
}
|
||||
|
||||
customResources[fmt.Sprintf("%s.yaml", namespacedName)] = b
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1630,7 +1637,7 @@ func getSelfSubjectRulesReviews(ctx context.Context, client *kubernetes.Clientse
|
||||
continue
|
||||
}
|
||||
|
||||
if response.Status.Incomplete == true {
|
||||
if response.Status.Incomplete {
|
||||
errorsByNamespace[namespace] = response.Status.EvaluationError
|
||||
}
|
||||
|
||||
@@ -2106,3 +2113,23 @@ func configMaps(ctx context.Context, client kubernetes.Interface, namespaces []s
|
||||
|
||||
return configmapByNamespace, errorsByNamespace
|
||||
}
|
||||
|
||||
// storeCustomResource stores a custom resource as JSON and YAML
|
||||
// We use both formats for backwards compatibility. This way we
|
||||
// avoid breaking existing tools and analysers that already rely on
|
||||
// the YAML format.
|
||||
func storeCustomResource(name string, objects any, m map[string][]byte) error {
|
||||
j, err := json.MarshalIndent(objects, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
y, err := yaml.Marshal(objects)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m[fmt.Sprintf("%s.json", name)] = j
|
||||
m[fmt.Sprintf("%s.yaml", name)] = y
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3,20 +3,29 @@ package collect
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/client/troubleshootclientset/scheme"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
v1 "k8s.io/api/coordination/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
storagev1 "k8s.io/api/storage/v1"
|
||||
apixfake "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
testdynamicclient "k8s.io/client-go/dynamic/fake"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
testclient "k8s.io/client-go/kubernetes/fake"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
func init() {
|
||||
apixfake.AddToScheme(scheme.Scheme)
|
||||
}
|
||||
|
||||
func Test_ConfigMaps(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -458,3 +467,59 @@ func TestClusterResources_Merge(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectClusterResources_CustomResource(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Register supportbundle troubleshoot CRD
|
||||
dat, err := os.ReadFile("../../config/crds/troubleshoot.sh_supportbundles.yaml")
|
||||
require.NoError(t, err)
|
||||
|
||||
obj, _, err := scheme.Codecs.UniversalDeserializer().Decode(dat, nil, nil)
|
||||
require.NoError(t, err)
|
||||
apixClient := apixfake.NewSimpleClientset(obj)
|
||||
|
||||
// Create a CR
|
||||
sbObject := troubleshootv1beta2.SupportBundle{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "supportbundle",
|
||||
Namespace: "default",
|
||||
},
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "SupportBundle",
|
||||
APIVersion: "troubleshoot.sh/v1beta2",
|
||||
},
|
||||
Spec: troubleshootv1beta2.SupportBundleSpec{
|
||||
Collectors: []*troubleshootv1beta2.Collect{
|
||||
{
|
||||
ClusterResources: &troubleshootv1beta2.ClusterResources{},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
dynamicClient := testdynamicclient.NewSimpleDynamicClient(scheme.Scheme, &sbObject)
|
||||
|
||||
// Fetch the CR from cluster
|
||||
res, errs := crsV1(ctx, dynamicClient, apixClient.ApiextensionsV1(), []string{"default"})
|
||||
assert.Empty(t, errs)
|
||||
require.Equal(t, 2, len(res))
|
||||
assert.Equal(t, fromJSON(t, res["supportbundles.troubleshoot.sh/default.json"]), sbObject)
|
||||
assert.Equal(t, fromYAML(t, res["supportbundles.troubleshoot.sh/default.yaml"]), sbObject)
|
||||
}
|
||||
|
||||
func fromYAML(t *testing.T, dat []byte) troubleshootv1beta2.SupportBundle {
|
||||
sb := []troubleshootv1beta2.SupportBundle{}
|
||||
err := yaml.Unmarshal(dat, &sb)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(sb))
|
||||
return sb[0]
|
||||
}
|
||||
|
||||
func fromJSON(t *testing.T, dat []byte) troubleshootv1beta2.SupportBundle {
|
||||
sb := []troubleshootv1beta2.SupportBundle{}
|
||||
err := json.Unmarshal(dat, &sb)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(sb))
|
||||
return sb[0]
|
||||
}
|
||||
|
||||
@@ -114,6 +114,8 @@ func GetCollector(collector *troubleshootv1beta2.Collect, bundlePath string, nam
|
||||
return &CollectSysctl{collector.Sysctl, bundlePath, namespace, clientConfig, client, ctx, RBACErrors}, true
|
||||
case collector.Certificates != nil:
|
||||
return &CollectCertificates{collector.Certificates, bundlePath, namespace, clientConfig, client, ctx, RBACErrors}, true
|
||||
case collector.Helm != nil:
|
||||
return &CollectHelm{collector.Helm, bundlePath, namespace, clientConfig, client, ctx, RBACErrors}, true
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
@@ -194,6 +196,8 @@ func getCollectorName(c interface{}) string {
|
||||
name = v.Collector.Name
|
||||
case *CollectCertificates:
|
||||
collector = "certificates"
|
||||
case *CollectHelm:
|
||||
collector = "helm"
|
||||
default:
|
||||
collector = "<none>"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
package collect
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
|
||||
"helm.sh/helm/v3/pkg/action"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
type CollectHelm struct {
|
||||
Collector *troubleshootv1beta2.Helm
|
||||
BundlePath string
|
||||
Namespace string
|
||||
ClientConfig *rest.Config
|
||||
Client kubernetes.Interface
|
||||
Context context.Context
|
||||
RBACErrors
|
||||
}
|
||||
|
||||
// Helm release information struct
|
||||
type ReleaseInfo struct {
|
||||
ReleaseName string `json:"releaseName"`
|
||||
Chart string `json:"chart,omitempty"`
|
||||
ChartVersion string `json:"chartVersion,omitempty"`
|
||||
AppVersion string `json:"appVersion,omitempty"`
|
||||
Namespace string `json:"namespace,omitempty"`
|
||||
VersionInfo []VersionInfo `json:"releaseHistory,omitempty"`
|
||||
}
|
||||
|
||||
// Helm release version information struct
|
||||
type VersionInfo struct {
|
||||
Revision string `json:"revision"`
|
||||
Date string `json:"date"`
|
||||
Status string `json:"status"`
|
||||
IsPending bool `json:"isPending,omitempty"`
|
||||
}
|
||||
|
||||
func (c *CollectHelm) Title() string {
|
||||
return getCollectorName(c)
|
||||
}
|
||||
|
||||
func (c *CollectHelm) IsExcluded() (bool, error) {
|
||||
return isExcluded(c.Collector.Exclude)
|
||||
}
|
||||
|
||||
func (c *CollectHelm) Collect(progressChan chan<- interface{}) (CollectorResult, error) {
|
||||
|
||||
output := NewResult()
|
||||
|
||||
releaseInfos, err := helmReleaseHistoryCollector(c.Collector.ReleaseName, c.Collector.Namespace)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get Helm release history")
|
||||
}
|
||||
|
||||
releaseInfoByNamespace := helmReleaseInfoByNamespaces(releaseInfos)
|
||||
|
||||
for namespace, releaseInfo := range releaseInfoByNamespace {
|
||||
|
||||
helmHistoryJson, errJson := json.MarshalIndent(releaseInfo, "", "\t")
|
||||
if errJson != nil {
|
||||
return nil, errJson
|
||||
}
|
||||
|
||||
filePath := fmt.Sprintf("helm/%s.json", namespace)
|
||||
|
||||
err := output.SaveResult(c.BundlePath, filePath, bytes.NewBuffer(helmHistoryJson))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func helmReleaseHistoryCollector(releaseName string, namespace string) ([]ReleaseInfo, error) {
|
||||
var results []ReleaseInfo
|
||||
|
||||
actionConfig := new(action.Configuration)
|
||||
if err := actionConfig.Init(nil, namespace, "", klog.V(2).Infof); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to initialize Helm action config")
|
||||
}
|
||||
|
||||
// If releaseName is specified, get the history of that release
|
||||
if releaseName != "" {
|
||||
getAction := action.NewGet(actionConfig)
|
||||
r, err := getAction.Run(releaseName)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to get Helm release %s", releaseName)
|
||||
}
|
||||
results = append(results, ReleaseInfo{
|
||||
ReleaseName: r.Name,
|
||||
Chart: r.Chart.Metadata.Name,
|
||||
ChartVersion: r.Chart.Metadata.Version,
|
||||
AppVersion: r.Chart.Metadata.AppVersion,
|
||||
Namespace: r.Namespace,
|
||||
VersionInfo: getVersionInfo(actionConfig, releaseName),
|
||||
})
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// If releaseName is not specified, get the history of all releases
|
||||
// If namespace is specified, get the history of all releases in that namespace
|
||||
// If namespace is not specified, get the history of all releases in all namespaces
|
||||
listAction := action.NewList(actionConfig)
|
||||
releases, err := listAction.Run()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to list Helm releases: %v", err)
|
||||
}
|
||||
|
||||
for _, r := range releases {
|
||||
results = append(results, ReleaseInfo{
|
||||
ReleaseName: r.Name,
|
||||
Chart: r.Chart.Metadata.Name,
|
||||
ChartVersion: r.Chart.Metadata.Version,
|
||||
AppVersion: r.Chart.Metadata.AppVersion,
|
||||
Namespace: r.Namespace,
|
||||
VersionInfo: getVersionInfo(actionConfig, r.Name),
|
||||
})
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func getVersionInfo(actionConfig *action.Configuration, releaseName string) []VersionInfo {
|
||||
|
||||
versionCollect := []VersionInfo{}
|
||||
|
||||
history, _ := action.NewHistory(actionConfig).Run(releaseName)
|
||||
|
||||
for _, release := range history {
|
||||
|
||||
versionCollect = append(versionCollect, VersionInfo{
|
||||
Revision: strconv.Itoa(release.Version),
|
||||
Date: release.Info.LastDeployed.String(),
|
||||
Status: release.Info.Status.String(),
|
||||
IsPending: release.Info.Status.IsPending(),
|
||||
})
|
||||
}
|
||||
return versionCollect
|
||||
}
|
||||
|
||||
func helmReleaseInfoByNamespaces(releaseInfo []ReleaseInfo) map[string][]ReleaseInfo {
|
||||
releaseInfoByNamespace := make(map[string][]ReleaseInfo)
|
||||
|
||||
for _, r := range releaseInfo {
|
||||
releaseInfoByNamespace[r.Namespace] = append(releaseInfoByNamespace[r.Namespace], r)
|
||||
}
|
||||
|
||||
return releaseInfoByNamespace
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package collect
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_HelmReleaseInfoByNamespaces(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
releaseInfo []ReleaseInfo
|
||||
want map[string][]ReleaseInfo
|
||||
}{
|
||||
{
|
||||
name: "Test case 1: Empty slice",
|
||||
releaseInfo: []ReleaseInfo{},
|
||||
want: map[string][]ReleaseInfo{},
|
||||
},
|
||||
{
|
||||
name: "Test case 2: Single namespace",
|
||||
releaseInfo: []ReleaseInfo{
|
||||
{ReleaseName: "release1", Namespace: "default"},
|
||||
{ReleaseName: "release2", Namespace: "default"},
|
||||
},
|
||||
want: map[string][]ReleaseInfo{
|
||||
"default": {
|
||||
{ReleaseName: "release1", Namespace: "default"},
|
||||
{ReleaseName: "release2", Namespace: "default"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Test case 3: Multiple namespaces",
|
||||
releaseInfo: []ReleaseInfo{
|
||||
{ReleaseName: "release1", Namespace: "default"},
|
||||
{ReleaseName: "release2", Namespace: "kube-system"},
|
||||
{ReleaseName: "release3", Namespace: "default"},
|
||||
},
|
||||
want: map[string][]ReleaseInfo{
|
||||
"default": {
|
||||
{ReleaseName: "release1", Namespace: "default"},
|
||||
{ReleaseName: "release3", Namespace: "default"},
|
||||
},
|
||||
"kube-system": {
|
||||
{ReleaseName: "release2", Namespace: "kube-system"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := helmReleaseInfoByNamespaces(tt.releaseInfo)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2,16 +2,35 @@ package collect
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/rand"
|
||||
"os/exec"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"golang.org/x/net/context"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
type Durations []time.Duration
|
||||
|
||||
func (d Durations) Len() int {
|
||||
return len(d)
|
||||
}
|
||||
|
||||
func (d Durations) Less(i, j int) bool {
|
||||
return d[i] < d[j]
|
||||
}
|
||||
|
||||
func (d Durations) Swap(i, j int) {
|
||||
d[i], d[j] = d[j], d[i]
|
||||
}
|
||||
|
||||
type CollectHostFilesystemPerformance struct {
|
||||
@@ -90,3 +109,324 @@ func (f FSPerfResults) String() string {
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
type FioResult struct {
|
||||
FioVersion string `json:"fio version,omitempty"`
|
||||
Timestamp int64 `json:"timestamp,omitempty"`
|
||||
TimestampMS int64 `json:"timestamp_ms,omitempty"`
|
||||
Time string `json:"time,omitempty"`
|
||||
GlobalOptions FioGlobalOptions `json:"global options,omitempty"`
|
||||
Jobs []FioJobs `json:"jobs,omitempty"`
|
||||
DiskUtil []FioDiskUtil `json:"disk_util,omitempty"`
|
||||
}
|
||||
|
||||
func (f FioResult) String() string {
|
||||
var res string
|
||||
res += fmt.Sprintf("FIO version - %s\n", f.FioVersion)
|
||||
res += fmt.Sprintf("Global options - %s\n\n", f.GlobalOptions)
|
||||
for _, job := range f.Jobs {
|
||||
res += fmt.Sprintf("%s\n", job)
|
||||
}
|
||||
res += "Disk stats (read/write):\n"
|
||||
for _, du := range f.DiskUtil {
|
||||
res += fmt.Sprintf("%s\n", du)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
type FioGlobalOptions struct {
|
||||
Directory string `json:"directory,omitempty"`
|
||||
RandRepeat string `json:"randrepeat,omitempty"`
|
||||
Verify string `json:"verify,omitempty"`
|
||||
IOEngine string `json:"ioengine,omitempty"`
|
||||
Direct string `json:"direct,omitempty"`
|
||||
GtodReduce string `json:"gtod_reduce,omitempty"`
|
||||
}
|
||||
|
||||
func (g FioGlobalOptions) String() string {
|
||||
return fmt.Sprintf("ioengine=%s verify=%s direct=%s gtod_reduce=%s", g.IOEngine, g.Verify, g.Direct, g.GtodReduce)
|
||||
}
|
||||
|
||||
type FioJobs struct {
|
||||
JobName string `json:"jobname,omitempty"`
|
||||
GroupID int `json:"groupid,omitempty"`
|
||||
Error int `json:"error,omitempty"`
|
||||
Eta int `json:"eta,omitempty"`
|
||||
Elapsed int `json:"elapsed,omitempty"`
|
||||
JobOptions FioJobOptions `json:"job options,omitempty"`
|
||||
Read FioStats `json:"read,omitempty"`
|
||||
Write FioStats `json:"write,omitempty"`
|
||||
Trim FioStats `json:"trim,omitempty"`
|
||||
Sync FioStats `json:"sync,omitempty"`
|
||||
JobRuntime int32 `json:"job_runtime,omitempty"`
|
||||
UsrCpu float32 `json:"usr_cpu,omitempty"`
|
||||
SysCpu float32 `json:"sys_cpu,omitempty"`
|
||||
Ctx int32 `json:"ctx,omitempty"`
|
||||
MajF int32 `json:"majf,omitempty"`
|
||||
MinF int32 `json:"minf,omitempty"`
|
||||
IoDepthLevel FioDepth `json:"iodepth_level,omitempty"`
|
||||
IoDepthSubmit FioDepth `json:"iodepth_submit,omitempty"`
|
||||
IoDepthComplete FioDepth `json:"iodepth_complete,omitempty"`
|
||||
LatencyNs FioLatency `json:"latency_ns,omitempty"`
|
||||
LatencyUs FioLatency `json:"latency_us,omitempty"`
|
||||
LatencyMs FioLatency `json:"latency_ms,omitempty"`
|
||||
LatencyDepth int32 `json:"latency_depth,omitempty"`
|
||||
LatencyTarget int32 `json:"latency_target,omitempty"`
|
||||
LatencyPercentile float32 `json:"latency_percentile,omitempty"`
|
||||
LatencyWindow int32 `json:"latency_window,omitempty"`
|
||||
}
|
||||
|
||||
func (j FioJobs) String() string {
|
||||
var job string
|
||||
job += fmt.Sprintf("%s\n", j.JobOptions)
|
||||
if j.Read.Iops != 0 || j.Read.BW != 0 {
|
||||
job += fmt.Sprintf("read:\n%s\n", j.Read)
|
||||
}
|
||||
if j.Write.Iops != 0 || j.Write.BW != 0 {
|
||||
job += fmt.Sprintf("write:\n%s\n", j.Write)
|
||||
}
|
||||
return job
|
||||
}
|
||||
|
||||
type FioJobOptions struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
BS string `json:"bs,omitempty"`
|
||||
Directory string `json:"directory,omitempty"`
|
||||
RW string `json:"rw,omitempty"`
|
||||
IOEngine string `json:"ioengine,omitempty"`
|
||||
FDataSync string `json:"fdatasync,omitempty"`
|
||||
Size string `json:"size,omitempty"`
|
||||
RunTime string `json:"runtime,omitempty"`
|
||||
}
|
||||
|
||||
func (o FioJobOptions) String() string {
|
||||
return fmt.Sprintf("JobName: %s\n blocksize=%s filesize=%s rw=%s", o.Name, o.BS, o.Size, o.RW)
|
||||
}
|
||||
|
||||
type FioStats struct {
|
||||
IOBytes int64 `json:"io_bytes,omitempty"`
|
||||
IOKBytes int64 `json:"io_kbytes,omitempty"`
|
||||
BWBytes int64 `json:"bw_bytes,omitempty"`
|
||||
BW int64 `json:"bw,omitempty"`
|
||||
Iops float32 `json:"iops,omitempty"`
|
||||
Runtime int64 `json:"runtime,omitempty"`
|
||||
TotalIos int64 `json:"total_ios,omitempty"`
|
||||
ShortIos int64 `json:"short_ios,omitempty"`
|
||||
DropIos int64 `json:"drop_ios,omitempty"`
|
||||
SlatNs FioNS `json:"slat_ns,omitempty"`
|
||||
ClatNs FioNS `json:"clat_ns,omitempty"`
|
||||
LatNs FioNS `json:"lat_ns,omitempty"`
|
||||
Percentile FioPercentile `json:"percentile,omitempty"`
|
||||
BwMin int64 `json:"bw_min,omitempty"`
|
||||
BwMax int64 `json:"bw_max,omitempty"`
|
||||
BwAgg float32 `json:"bw_agg,omitempty"`
|
||||
BwMean float32 `json:"bw_mean,omitempty"`
|
||||
BwDev float32 `json:"bw_dev,omitempty"`
|
||||
BwSamples int32 `json:"bw_samples,omitempty"`
|
||||
IopsMin int32 `json:"iops_min,omitempty"`
|
||||
IopsMax int32 `json:"iops_max,omitempty"`
|
||||
IopsMean float32 `json:"iops_mean,omitempty"`
|
||||
IopsStdDev float32 `json:"iops_stddev,omitempty"`
|
||||
IopsSamples int32 `json:"iops_samples,omitempty"`
|
||||
}
|
||||
|
||||
func (s FioStats) String() string {
|
||||
var stats string
|
||||
stats += fmt.Sprintf(" IOPS=%f BW(KiB/s)=%d\n", s.Iops, s.BW)
|
||||
stats += fmt.Sprintf(" iops: min=%d max=%d avg=%f\n", s.IopsMin, s.IopsMax, s.IopsMean)
|
||||
stats += fmt.Sprintf(" bw(KiB/s): min=%d max=%d avg=%f", s.BwMin, s.BwMax, s.BwMean)
|
||||
return stats
|
||||
}
|
||||
|
||||
func (s FioStats) FSPerfResults() FSPerfResults {
|
||||
return FSPerfResults{
|
||||
Min: time.Duration(s.LatNs.Min),
|
||||
Max: time.Duration(s.LatNs.Max),
|
||||
Average: time.Duration(s.LatNs.Mean),
|
||||
P1: time.Duration(s.LatNs.Percentile.P1),
|
||||
P5: time.Duration(s.LatNs.Percentile.P5),
|
||||
P10: time.Duration(s.LatNs.Percentile.P10),
|
||||
P20: time.Duration(s.LatNs.Percentile.P20),
|
||||
P30: time.Duration(s.LatNs.Percentile.P30),
|
||||
P40: time.Duration(s.LatNs.Percentile.P40),
|
||||
P50: time.Duration(s.LatNs.Percentile.P50),
|
||||
P60: time.Duration(s.LatNs.Percentile.P60),
|
||||
P70: time.Duration(s.LatNs.Percentile.P70),
|
||||
P80: time.Duration(s.LatNs.Percentile.P80),
|
||||
P90: time.Duration(s.LatNs.Percentile.P90),
|
||||
P95: time.Duration(s.LatNs.Percentile.P95),
|
||||
P99: time.Duration(s.LatNs.Percentile.P99),
|
||||
P995: time.Duration(s.LatNs.Percentile.P995),
|
||||
P999: time.Duration(s.LatNs.Percentile.P999),
|
||||
P9995: time.Duration(s.LatNs.Percentile.P9995),
|
||||
P9999: time.Duration(s.LatNs.Percentile.P9999),
|
||||
}
|
||||
}
|
||||
|
||||
type FioNS struct {
|
||||
Min int64 `json:"min,omitempty"`
|
||||
Max int64 `json:"max,omitempty"`
|
||||
Mean float32 `json:"mean,omitempty"`
|
||||
StdDev float32 `json:"stddev,omitempty"`
|
||||
N int64 `json:"N,omitempty"`
|
||||
Percentile FioPercentile `json:"percentile,omitempty"`
|
||||
}
|
||||
|
||||
type FioDepth struct {
|
||||
FioDepth0 float32 `json:"0,omitempty"`
|
||||
FioDepth1 float32 `json:"1,omitempty"`
|
||||
FioDepth2 float32 `json:"2,omitempty"`
|
||||
FioDepth4 float32 `json:"4,omitempty"`
|
||||
FioDepth8 float32 `json:"8,omitempty"`
|
||||
FioDepth16 float32 `json:"16,omitempty"`
|
||||
FioDepth32 float32 `json:"32,omitempty"`
|
||||
FioDepth64 float32 `json:"64,omitempty"`
|
||||
FioDepthGE64 float32 `json:">=64,omitempty"`
|
||||
}
|
||||
|
||||
type FioLatency struct {
|
||||
FioLat2 float32 `json:"2,omitempty"`
|
||||
FioLat4 float32 `json:"4,omitempty"`
|
||||
FioLat10 float32 `json:"10,omitempty"`
|
||||
FioLat20 float32 `json:"20,omitempty"`
|
||||
FioLat50 float32 `json:"50,omitempty"`
|
||||
FioLat100 float32 `json:"100,omitempty"`
|
||||
FioLat250 float32 `json:"250,omitempty"`
|
||||
FioLat500 float32 `json:"500,omitempty"`
|
||||
FioLat750 float32 `json:"750,omitempty"`
|
||||
FioLat1000 float32 `json:"1000,omitempty"`
|
||||
FioLat2000 float32 `json:"2000,omitempty"`
|
||||
FioLatGE2000 float32 `json:">=2000,omitempty"`
|
||||
}
|
||||
|
||||
type FioDiskUtil struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
ReadIos int64 `json:"read_ios,omitempty"`
|
||||
WriteIos int64 `json:"write_ios,omitempty"`
|
||||
ReadMerges int64 `json:"read_merges,omitempty"`
|
||||
WriteMerges int64 `json:"write_merges,omitempty"`
|
||||
ReadTicks int64 `json:"read_ticks,omitempty"`
|
||||
WriteTicks int64 `json:"write_ticks,omitempty"`
|
||||
InQueue int64 `json:"in_queue,omitempty"`
|
||||
Util float32 `json:"util,omitempty"`
|
||||
}
|
||||
|
||||
type FioPercentile struct {
|
||||
P1 int `json:"1.000000,omitempty"`
|
||||
P5 int `json:"5.000000,omitempty"`
|
||||
P10 int `json:"10.000000,omitempty"`
|
||||
P20 int `json:"20.000000,omitempty"`
|
||||
P30 int `json:"30.000000,omitempty"`
|
||||
P40 int `json:"40.000000,omitempty"`
|
||||
P50 int `json:"50.000000,omitempty"`
|
||||
P60 int `json:"60.000000,omitempty"`
|
||||
P70 int `json:"70.000000,omitempty"`
|
||||
P80 int `json:"80.000000,omitempty"`
|
||||
P90 int `json:"90.000000,omitempty"`
|
||||
P95 int `json:"95.000000,omitempty"`
|
||||
P99 int `json:"99.000000,omitempty"`
|
||||
P995 int `json:"99.500000,omitempty"`
|
||||
P999 int `json:"99.900000,omitempty"`
|
||||
P9995 int `json:"99.950000,omitempty"`
|
||||
P9999 int `json:"99.990000,omitempty"`
|
||||
}
|
||||
|
||||
func (d FioDiskUtil) String() string {
|
||||
//Disk stats (read/write):
|
||||
//rbd4: ios=30022/11982, merge=0/313, ticks=1028675/1022768, in_queue=2063740, util=99.67%
|
||||
var du string
|
||||
du += fmt.Sprintf(" %s: ios=%d/%d merge=%d/%d ticks=%d/%d in_queue=%d, util=%f%%", d.Name, d.ReadIos,
|
||||
d.WriteIos, d.ReadMerges, d.WriteMerges, d.ReadTicks, d.WriteTicks, d.InQueue, d.Util)
|
||||
return du
|
||||
}
|
||||
|
||||
func parseCollectorOptions(hostCollector *troubleshootv1beta2.FilesystemPerformance) ([]string, *FioJobOptions, error) {
|
||||
|
||||
var operationSize uint64 = 1024
|
||||
if hostCollector.OperationSizeBytes > 0 {
|
||||
operationSize = hostCollector.OperationSizeBytes
|
||||
}
|
||||
var fileSize uint64 = 10 * 1024 * 1024
|
||||
if hostCollector.FileSize != "" {
|
||||
quantity, err := resource.ParseQuantity(hostCollector.FileSize)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrapf(err, "failed to parse fileSize %q", hostCollector.FileSize)
|
||||
}
|
||||
fileSizeInt64, ok := quantity.AsInt64()
|
||||
if !ok {
|
||||
return nil, nil, errors.Wrapf(err, "failed to parse fileSize %q", hostCollector.FileSize)
|
||||
}
|
||||
if fileSizeInt64 <= 0 {
|
||||
return nil, nil, errors.Wrapf(err, "fileSize %q must be greater than 0", hostCollector.FileSize)
|
||||
}
|
||||
fileSize = uint64(fileSizeInt64)
|
||||
}
|
||||
|
||||
if hostCollector.Directory == "" {
|
||||
return nil, nil, errors.New("Directory is required to collect filesystem performance info")
|
||||
}
|
||||
|
||||
latencyBenchmarkOptions := FioJobOptions{
|
||||
RW: "write",
|
||||
IOEngine: "sync",
|
||||
FDataSync: "1",
|
||||
Directory: hostCollector.Directory,
|
||||
Size: strconv.FormatUint(fileSize, 10),
|
||||
BS: strconv.FormatUint(operationSize, 10),
|
||||
Name: "fsperf",
|
||||
RunTime: "120",
|
||||
}
|
||||
|
||||
command := buildFioCommand(latencyBenchmarkOptions)
|
||||
|
||||
return command, &latencyBenchmarkOptions, nil
|
||||
}
|
||||
|
||||
func buildFioCommand(opts FioJobOptions) []string {
|
||||
command := []string{"fio"}
|
||||
v := reflect.ValueOf(opts)
|
||||
t := reflect.TypeOf(opts)
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
value := v.Field(i)
|
||||
if !value.IsZero() {
|
||||
command = append(command, fmt.Sprintf("--%s=%v", strings.ToLower(field.Name), value.Interface()))
|
||||
}
|
||||
}
|
||||
command = append(command, "--output-format=json")
|
||||
return command
|
||||
}
|
||||
|
||||
func collectFioResults(ctx context.Context, hostCollector *troubleshootv1beta2.FilesystemPerformance) (*FioResult, error) {
|
||||
|
||||
command, opts, err := parseCollectorOptions(hostCollector)
|
||||
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to parse collector options")
|
||||
}
|
||||
|
||||
klog.V(2).Infof("collecting fio results: %s", strings.Join(command, " "))
|
||||
output, err := exec.CommandContext(ctx, command[0], command[1:]...).Output()
|
||||
if err != nil {
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
if exitErr.ExitCode() == 1 {
|
||||
return nil, errors.Wrapf(err, "fio failed; permission denied opening %s. ensure this collector runs as root", opts.Directory)
|
||||
} else {
|
||||
return nil, errors.Wrapf(err, "fio failed with exit status %d", exitErr.ExitCode())
|
||||
}
|
||||
} else if e, ok := err.(*exec.Error); ok && e.Err == exec.ErrNotFound {
|
||||
return nil, errors.Wrapf(err, "command not found: %v. ensure fio is installed", command)
|
||||
} else {
|
||||
return nil, errors.Wrapf(err, "failed to run command: %v", command)
|
||||
}
|
||||
}
|
||||
|
||||
var result FioResult
|
||||
err = json.Unmarshal([]byte(output), &result)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to unmarshal fio result")
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
@@ -10,36 +10,22 @@ import (
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
}
|
||||
|
||||
type Durations []time.Duration
|
||||
|
||||
func (d Durations) Len() int {
|
||||
return len(d)
|
||||
}
|
||||
|
||||
func (d Durations) Less(i, j int) bool {
|
||||
return d[i] < d[j]
|
||||
}
|
||||
|
||||
func (d Durations) Swap(i, j int) {
|
||||
d[i], d[j] = d[j], d[i]
|
||||
}
|
||||
// Today we only care about checking for write latency so the options struct
|
||||
// only has what we need for that. we'll collect all the results from a single run of fio
|
||||
// and filter out the fsync results for analysis. TODO: update the analyzer so any/all results
|
||||
// from fio can be analyzed.
|
||||
|
||||
func collectHostFilesystemPerformance(hostCollector *troubleshootv1beta2.FilesystemPerformance, bundlePath string) (map[string][]byte, error) {
|
||||
timeout := time.Minute
|
||||
|
||||
if hostCollector.Timeout != "" {
|
||||
d, err := time.ParseDuration(hostCollector.Timeout)
|
||||
if err != nil {
|
||||
@@ -50,46 +36,15 @@ func collectHostFilesystemPerformance(hostCollector *troubleshootv1beta2.Filesys
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
var operationSize uint64 = 1024
|
||||
if hostCollector.OperationSizeBytes != 0 {
|
||||
operationSize = hostCollector.OperationSizeBytes
|
||||
collectorName := hostCollector.CollectorName
|
||||
if collectorName == "" {
|
||||
collectorName = "filesystemPerformance"
|
||||
}
|
||||
name := filepath.Join("host-collectors/filesystemPerformance", collectorName+".json")
|
||||
|
||||
var fileSize uint64 = 10 * 1024 * 1024
|
||||
if hostCollector.FileSize != "" {
|
||||
quantity, err := resource.ParseQuantity(hostCollector.FileSize)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to parse fileSize %q", hostCollector.FileSize)
|
||||
}
|
||||
fileSizeInt64, ok := quantity.AsInt64()
|
||||
if !ok {
|
||||
return nil, errors.Wrapf(err, "failed to parse fileSize %q", hostCollector.FileSize)
|
||||
}
|
||||
fileSize = uint64(fileSizeInt64)
|
||||
}
|
||||
|
||||
if hostCollector.Directory == "" {
|
||||
return nil, errors.New("Directory is required to collect filesystem performance info")
|
||||
}
|
||||
// TODO: clean up this directory if its created
|
||||
if err := os.MkdirAll(hostCollector.Directory, 0700); err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to mkdir %q", hostCollector.Directory)
|
||||
}
|
||||
filename := filepath.Join(hostCollector.Directory, "fsperf")
|
||||
|
||||
f, err := os.OpenFile(filename, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600)
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
return nil, errors.Wrapf(err, "open %s", filename)
|
||||
}
|
||||
defer func() {
|
||||
if err := f.Close(); err != nil {
|
||||
log.Println(err.Error())
|
||||
}
|
||||
if err := os.Remove(filename); err != nil {
|
||||
log.Println(err.Error())
|
||||
}
|
||||
}()
|
||||
|
||||
// Start the background IOPS task and wait for warmup
|
||||
if hostCollector.EnableBackgroundIOPS {
|
||||
@@ -123,86 +78,16 @@ func collectHostFilesystemPerformance(hostCollector *troubleshootv1beta2.Filesys
|
||||
time.Sleep(time.Second * time.Duration(hostCollector.BackgroundIOPSWarmupSeconds))
|
||||
}
|
||||
|
||||
// Sequential writes benchmark
|
||||
var written uint64 = 0
|
||||
var results Durations
|
||||
var fioResult *FioResult
|
||||
|
||||
for {
|
||||
if written >= fileSize {
|
||||
break
|
||||
}
|
||||
|
||||
data := make([]byte, int(operationSize))
|
||||
rand.Read(data)
|
||||
|
||||
start := time.Now()
|
||||
|
||||
n, err := f.Write(data)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "write to %s", filename)
|
||||
}
|
||||
if hostCollector.Sync {
|
||||
if err := f.Sync(); err != nil {
|
||||
return nil, errors.Wrapf(err, "sync %s", filename)
|
||||
}
|
||||
} else if hostCollector.Datasync {
|
||||
if err := syscall.Fdatasync(int(f.Fd())); err != nil {
|
||||
return nil, errors.Wrapf(err, "datasync %s", filename)
|
||||
}
|
||||
}
|
||||
|
||||
d := time.Now().Sub(start)
|
||||
results = append(results, d)
|
||||
|
||||
written += uint64(n)
|
||||
|
||||
if ctx.Err() != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if len(results) == 0 {
|
||||
return nil, errors.New("No filesystem performance results collected")
|
||||
}
|
||||
|
||||
sort.Sort(results)
|
||||
|
||||
var sum time.Duration
|
||||
for _, d := range results {
|
||||
sum += d
|
||||
}
|
||||
|
||||
fsPerf := &FSPerfResults{
|
||||
Min: results[0],
|
||||
Max: results[len(results)-1],
|
||||
Average: sum / time.Duration(len(results)),
|
||||
P1: results[getPercentileIndex(.01, len(results))],
|
||||
P5: results[getPercentileIndex(.05, len(results))],
|
||||
P10: results[getPercentileIndex(.1, len(results))],
|
||||
P20: results[getPercentileIndex(.2, len(results))],
|
||||
P30: results[getPercentileIndex(.3, len(results))],
|
||||
P40: results[getPercentileIndex(.4, len(results))],
|
||||
P50: results[getPercentileIndex(.5, len(results))],
|
||||
P60: results[getPercentileIndex(.6, len(results))],
|
||||
P70: results[getPercentileIndex(.7, len(results))],
|
||||
P80: results[getPercentileIndex(.8, len(results))],
|
||||
P90: results[getPercentileIndex(.9, len(results))],
|
||||
P95: results[getPercentileIndex(.95, len(results))],
|
||||
P99: results[getPercentileIndex(.99, len(results))],
|
||||
P995: results[getPercentileIndex(.995, len(results))],
|
||||
P999: results[getPercentileIndex(.999, len(results))],
|
||||
P9995: results[getPercentileIndex(.9995, len(results))],
|
||||
P9999: results[getPercentileIndex(.9999, len(results))],
|
||||
}
|
||||
|
||||
collectorName := hostCollector.CollectorName
|
||||
if collectorName == "" {
|
||||
collectorName = "filesystemPerformance"
|
||||
}
|
||||
name := filepath.Join("host-collectors/filesystemPerformance", collectorName+".json")
|
||||
b, err := json.Marshal(fsPerf)
|
||||
fioResult, err := collectFioResults(ctx, hostCollector)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to marshal fs perf results")
|
||||
return nil, errors.Wrap(err, "failed to collect fio results")
|
||||
}
|
||||
|
||||
b, err := json.Marshal(fioResult)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to unmarshal fio results")
|
||||
}
|
||||
|
||||
output := NewResult()
|
||||
|
||||
@@ -2,7 +2,10 @@ package collect
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
)
|
||||
|
||||
func TestGetPercentileIndex(t *testing.T) {
|
||||
@@ -57,3 +60,128 @@ func TestGetPercentileIndex(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_parseCollectorOptions(t *testing.T) {
|
||||
type args struct {
|
||||
hostCollector *troubleshootv1beta2.FilesystemPerformance
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantCommand []string
|
||||
wantOptions *FioJobOptions
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Happy spec",
|
||||
args: args{
|
||||
hostCollector: &troubleshootv1beta2.FilesystemPerformance{
|
||||
HostCollectorMeta: troubleshootv1beta2.HostCollectorMeta{
|
||||
CollectorName: "fsperf",
|
||||
},
|
||||
OperationSizeBytes: 1024,
|
||||
Directory: "/var/lib/etcd",
|
||||
FileSize: "22Mi",
|
||||
Sync: true,
|
||||
Datasync: true,
|
||||
Timeout: "120",
|
||||
EnableBackgroundIOPS: true,
|
||||
BackgroundIOPSWarmupSeconds: 10,
|
||||
BackgroundWriteIOPS: 100,
|
||||
BackgroundReadIOPS: 100,
|
||||
BackgroundWriteIOPSJobs: 1,
|
||||
BackgroundReadIOPSJobs: 1,
|
||||
},
|
||||
},
|
||||
wantCommand: []string{
|
||||
"fio",
|
||||
"--name=fsperf",
|
||||
"--bs=1024",
|
||||
"--directory=/var/lib/etcd",
|
||||
"--rw=write",
|
||||
"--ioengine=sync",
|
||||
"--fdatasync=1",
|
||||
"--size=23068672",
|
||||
"--runtime=120",
|
||||
"--output-format=json",
|
||||
},
|
||||
wantOptions: &FioJobOptions{
|
||||
RW: "write",
|
||||
IOEngine: "sync",
|
||||
FDataSync: "1",
|
||||
Directory: "/var/lib/etcd",
|
||||
Size: "23068672",
|
||||
BS: "1024",
|
||||
Name: "fsperf",
|
||||
RunTime: "120",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Empty spec fails",
|
||||
args: args{
|
||||
hostCollector: &troubleshootv1beta2.FilesystemPerformance{
|
||||
HostCollectorMeta: troubleshootv1beta2.HostCollectorMeta{
|
||||
CollectorName: "fsperf",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantCommand: nil,
|
||||
wantOptions: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Invalid filesize",
|
||||
args: args{
|
||||
hostCollector: &troubleshootv1beta2.FilesystemPerformance{
|
||||
HostCollectorMeta: troubleshootv1beta2.HostCollectorMeta{
|
||||
CollectorName: "fsperf",
|
||||
},
|
||||
OperationSizeBytes: 1024,
|
||||
Directory: "/var/lib/etcd",
|
||||
FileSize: "abcd",
|
||||
Sync: true,
|
||||
Datasync: true,
|
||||
Timeout: "120",
|
||||
},
|
||||
},
|
||||
wantCommand: nil,
|
||||
wantOptions: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid path parameter",
|
||||
args: args{
|
||||
hostCollector: &troubleshootv1beta2.FilesystemPerformance{
|
||||
HostCollectorMeta: troubleshootv1beta2.HostCollectorMeta{
|
||||
CollectorName: "fsperf",
|
||||
},
|
||||
OperationSizeBytes: 1024,
|
||||
Directory: "",
|
||||
FileSize: "22Mi",
|
||||
Sync: true,
|
||||
Datasync: true,
|
||||
Timeout: "120",
|
||||
},
|
||||
},
|
||||
wantCommand: nil,
|
||||
wantOptions: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotCommand, gotOptions, err := parseCollectorOptions(tt.args.hostCollector)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("parseCollectorOptions() error = %v, wantErr %v", err, tt.wantErr)
|
||||
} else {
|
||||
if !reflect.DeepEqual(gotCommand, tt.wantCommand) {
|
||||
t.Errorf("parseCollectorOptions() got command = %v, want %v", gotCommand, tt.wantCommand)
|
||||
}
|
||||
if !reflect.DeepEqual(gotOptions, tt.wantOptions) {
|
||||
t.Errorf("parseCollectorOptions() got options = %v, want %v", gotOptions, tt.wantOptions)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,13 +28,20 @@ func (c *CollectHostHTTP) Collect(progressChan chan<- interface{}) (map[string][
|
||||
var response *http.Response
|
||||
var err error
|
||||
|
||||
if httpCollector.Get != nil {
|
||||
response, err = doGet(httpCollector.Get)
|
||||
} else if httpCollector.Post != nil {
|
||||
response, err = doPost(httpCollector.Post)
|
||||
} else if httpCollector.Put != nil {
|
||||
response, err = doPut(httpCollector.Put)
|
||||
} else {
|
||||
switch {
|
||||
case httpCollector.Get != nil:
|
||||
response, err = doRequest(
|
||||
"GET", httpCollector.Get.URL, httpCollector.Get.Headers,
|
||||
"", httpCollector.Get.InsecureSkipVerify, httpCollector.Get.Timeout)
|
||||
case httpCollector.Post != nil:
|
||||
response, err = doRequest(
|
||||
"POST", httpCollector.Post.URL, httpCollector.Post.Headers,
|
||||
httpCollector.Post.Body, httpCollector.Post.InsecureSkipVerify, httpCollector.Post.Timeout)
|
||||
case httpCollector.Put != nil:
|
||||
response, err = doRequest(
|
||||
"PUT", httpCollector.Put.URL, httpCollector.Put.Headers,
|
||||
httpCollector.Put.Body, httpCollector.Put.InsecureSkipVerify, httpCollector.Put.Timeout)
|
||||
default:
|
||||
return nil, errors.New("no supported http request type")
|
||||
}
|
||||
|
||||
|
||||
+162
-21
@@ -3,18 +3,24 @@ package collect
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
type HostRunInfo struct {
|
||||
Command string `json:"command"`
|
||||
ExitCode string `json:"exitCode"`
|
||||
Error string `json:"error"`
|
||||
Command string `json:"command"`
|
||||
ExitCode string `json:"exitCode"`
|
||||
Error string `json:"error"`
|
||||
OutputDir string `json:"outputDir"`
|
||||
Input string `json:"input"`
|
||||
Env []string `json:"env"`
|
||||
}
|
||||
|
||||
type CollectHostRun struct {
|
||||
@@ -31,20 +37,83 @@ func (c *CollectHostRun) IsExcluded() (bool, error) {
|
||||
}
|
||||
|
||||
func (c *CollectHostRun) Collect(progressChan chan<- interface{}) (map[string][]byte, error) {
|
||||
runHostCollector := c.hostCollector
|
||||
var (
|
||||
cmdOutputTempDir string
|
||||
cmdInputTempDir string
|
||||
bundleOutputRelativePath string
|
||||
)
|
||||
|
||||
cmd := exec.Command(runHostCollector.Command, runHostCollector.Args...)
|
||||
runHostCollector := c.hostCollector
|
||||
collectorName := runHostCollector.CollectorName
|
||||
if collectorName == "" {
|
||||
collectorName = "run-host"
|
||||
}
|
||||
|
||||
cmd := exec.Command(c.attemptToConvertCmdToAbsPath(), runHostCollector.Args...)
|
||||
|
||||
klog.V(2).Infof("Run host collector command: %q", cmd.String())
|
||||
runInfo := &HostRunInfo{
|
||||
Command: cmd.String(),
|
||||
ExitCode: "0",
|
||||
}
|
||||
|
||||
err := c.processEnvVars(cmd)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to parse env variable")
|
||||
}
|
||||
|
||||
// Create a working directory for the command
|
||||
wkdir, err := os.MkdirTemp("", collectorName)
|
||||
defer os.RemoveAll(wkdir)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create temp dir for host run")
|
||||
}
|
||||
// Change the working directory for the command to ensure the command
|
||||
// does not polute the parent/caller working directory
|
||||
cmd.Dir = wkdir
|
||||
|
||||
// if we choose to save result for the command run
|
||||
if runHostCollector.OutputDir != "" {
|
||||
cmdOutputTempDir = filepath.Join(wkdir, runHostCollector.OutputDir)
|
||||
err = os.MkdirAll(cmdOutputTempDir, 0755)
|
||||
if err != nil {
|
||||
return nil, errors.New(fmt.Sprintf("failed to create dir for: %s", runHostCollector.OutputDir))
|
||||
}
|
||||
cmd.Env = append(cmd.Env,
|
||||
fmt.Sprintf("TS_OUTPUT_DIR=%s", cmdOutputTempDir),
|
||||
)
|
||||
}
|
||||
|
||||
if runHostCollector.Input != nil {
|
||||
cmdInputTempDir = filepath.Join(wkdir, "input")
|
||||
err = os.MkdirAll(cmdInputTempDir, 0755)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to create temp dir for host run input")
|
||||
}
|
||||
for inFilename, inFileContent := range runHostCollector.Input {
|
||||
if strings.Contains(inFilename, "/") {
|
||||
return nil, errors.New("Input filename contains '/'")
|
||||
}
|
||||
cmdInputFilePath := filepath.Join(cmdInputTempDir, inFilename)
|
||||
err = os.WriteFile(cmdInputFilePath, []byte(inFileContent), 0644)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, fmt.Sprintf("failed to write input file: %s to temp directory", inFilename))
|
||||
}
|
||||
}
|
||||
cmd.Env = append(cmd.Env,
|
||||
fmt.Sprintf("TS_INPUT_DIR=%s", cmdInputTempDir),
|
||||
)
|
||||
}
|
||||
|
||||
collectorRelativePath := filepath.Join("host-collectors/run-host", collectorName)
|
||||
|
||||
runInfo.Env = cmd.Env
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
runInfo := HostRunInfo{
|
||||
Command: cmd.String(),
|
||||
ExitCode: "0",
|
||||
}
|
||||
|
||||
err := cmd.Run()
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
if werr, ok := err.(*exec.ExitError); ok {
|
||||
runInfo.ExitCode = strings.TrimPrefix(werr.Error(), "exit status ")
|
||||
@@ -54,10 +123,7 @@ func (c *CollectHostRun) Collect(progressChan chan<- interface{}) (map[string][]
|
||||
}
|
||||
}
|
||||
|
||||
collectorName := c.hostCollector.CollectorName
|
||||
if collectorName == "" {
|
||||
collectorName = "run-host"
|
||||
}
|
||||
output := NewResult()
|
||||
resultInfo := filepath.Join("host-collectors/run-host", collectorName+"-info.json")
|
||||
result := filepath.Join("host-collectors/run-host", collectorName+".txt")
|
||||
|
||||
@@ -66,14 +132,89 @@ func (c *CollectHostRun) Collect(progressChan chan<- interface{}) (map[string][]
|
||||
return nil, errors.Wrap(err, "failed to marshal run host result")
|
||||
}
|
||||
|
||||
output := NewResult()
|
||||
output.SaveResult(c.BundlePath, resultInfo, bytes.NewBuffer(b))
|
||||
output.SaveResult(c.BundlePath, result, bytes.NewBuffer(stdout.Bytes()))
|
||||
|
||||
runHostOutput := map[string][]byte{
|
||||
resultInfo: b,
|
||||
result: stdout.Bytes(),
|
||||
// walkthrough the output directory and save result for each file
|
||||
if runHostCollector.OutputDir != "" {
|
||||
runInfo.OutputDir = runHostCollector.OutputDir
|
||||
bundleOutputRelativePath = filepath.Join(collectorRelativePath, runHostCollector.OutputDir)
|
||||
klog.V(2).Infof("Saving command output to %q in bundle", bundleOutputRelativePath)
|
||||
output.SaveResults(c.BundlePath, bundleOutputRelativePath, cmdOutputTempDir)
|
||||
}
|
||||
|
||||
return runHostOutput, nil
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func (c *CollectHostRun) processEnvVars(cmd *exec.Cmd) error {
|
||||
runHostCollector := c.hostCollector
|
||||
|
||||
if runHostCollector.IgnoreParentEnvs {
|
||||
klog.V(2).Info("Not inheriting the environment variables!")
|
||||
if runHostCollector.InheritEnvs != nil {
|
||||
klog.V(2).Infof("The following environment variables will not be loaded to the command: [%s]",
|
||||
strings.Join(runHostCollector.InheritEnvs, ","))
|
||||
}
|
||||
// clears the parent env vars
|
||||
cmd.Env = []string{}
|
||||
populateGuaranteedEnvVars(cmd)
|
||||
} else if runHostCollector.InheritEnvs != nil {
|
||||
for _, key := range runHostCollector.InheritEnvs {
|
||||
envVal, found := os.LookupEnv(key)
|
||||
if !found {
|
||||
return errors.New(fmt.Sprintf("inherit env variable is not found: %s", key))
|
||||
}
|
||||
cmd.Env = append(cmd.Env,
|
||||
fmt.Sprintf("%s=%s", key, envVal))
|
||||
}
|
||||
populateGuaranteedEnvVars(cmd)
|
||||
} else {
|
||||
cmd.Env = os.Environ()
|
||||
}
|
||||
|
||||
if runHostCollector.Env != nil {
|
||||
for i := range runHostCollector.Env {
|
||||
parts := strings.Split(runHostCollector.Env[i], "=")
|
||||
if len(parts) == 2 && parts[0] != "" && parts[1] != "" {
|
||||
cmd.Env = append(cmd.Env,
|
||||
fmt.Sprintf("%s", runHostCollector.Env[i]))
|
||||
} else {
|
||||
return errors.New(fmt.Sprintf("env variable entry is missing '=' : %s", runHostCollector.Env[i]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func populateGuaranteedEnvVars(cmd *exec.Cmd) {
|
||||
guaranteedEnvs := []string{"PATH", "KUBECONFIG", "PWD"}
|
||||
for _, key := range guaranteedEnvs {
|
||||
guaranteedEnvVal, found := os.LookupEnv(key)
|
||||
if found {
|
||||
cmd.Env = append(cmd.Env,
|
||||
fmt.Sprintf("%s=%s", key, guaranteedEnvVal))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// attemptToConvertCmdToAbsPath checks if the command is a file path or command name
|
||||
// If it is a file path, it will return the absolute path else
|
||||
// it will return the command name as is and leave the resolution to cmd.Run()
|
||||
// This enables passing commands using relative paths e.g. "./my-command"
|
||||
// which is not possible with cmd.Run() since the child process runs
|
||||
// in a different working directory
|
||||
func (c *CollectHostRun) attemptToConvertCmdToAbsPath() string {
|
||||
// Attempt to check if the command is file path or command name
|
||||
cmdAbsPath, err := filepath.Abs(c.hostCollector.Command)
|
||||
if err != nil {
|
||||
return c.hostCollector.Command
|
||||
}
|
||||
|
||||
// Check if the file exists
|
||||
_, err = os.Stat(cmdAbsPath)
|
||||
if err != nil {
|
||||
return c.hostCollector.Command
|
||||
}
|
||||
|
||||
return cmdAbsPath
|
||||
}
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
package collect
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestProcessEnv(t *testing.T) {
|
||||
pathEnv := fmt.Sprintf("%s=%s", "PATH", os.Getenv("PATH"))
|
||||
pwdEnv := fmt.Sprintf("%s=%s", "PWD", os.Getenv("PWD"))
|
||||
homeEnv := fmt.Sprintf("%s=%s", "HOME", os.Getenv("HOME"))
|
||||
os.Setenv("KUBECONFIG", "/some/kubeconfig")
|
||||
kubeconfigEnv := fmt.Sprintf("%s=%s", "KUBECONFIG", os.Getenv("KUBECONFIG"))
|
||||
tests := []struct {
|
||||
name string
|
||||
collector *CollectHostRun
|
||||
parentEnv []string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "setting Env field",
|
||||
collector: &CollectHostRun{
|
||||
hostCollector: &troubleshootv1beta2.HostRun{
|
||||
Env: []string{
|
||||
"USER=dummy",
|
||||
"AWS_REGION=us-east-1",
|
||||
},
|
||||
},
|
||||
BundlePath: "",
|
||||
},
|
||||
want: append([]string{
|
||||
"USER=dummy",
|
||||
"AWS_REGION=us-east-1",
|
||||
}, os.Environ()...),
|
||||
},
|
||||
{
|
||||
name: "guaranteed env",
|
||||
collector: &CollectHostRun{
|
||||
hostCollector: &troubleshootv1beta2.HostRun{
|
||||
IgnoreParentEnvs: true,
|
||||
},
|
||||
BundlePath: "",
|
||||
},
|
||||
want: []string{pathEnv, pwdEnv, kubeconfigEnv},
|
||||
},
|
||||
{
|
||||
name: "ignoring parent env",
|
||||
collector: &CollectHostRun{
|
||||
hostCollector: &troubleshootv1beta2.HostRun{
|
||||
Env: []string{
|
||||
"USER=dummy",
|
||||
"AWS_REGION=us-east-1",
|
||||
},
|
||||
IgnoreParentEnvs: true,
|
||||
// inheritEnv will be ignored if IgnoreParentEnvs is true
|
||||
InheritEnvs: []string{
|
||||
"HOME",
|
||||
},
|
||||
},
|
||||
BundlePath: "",
|
||||
},
|
||||
want: append([]string{
|
||||
"USER=dummy",
|
||||
"AWS_REGION=us-east-1",
|
||||
}, pathEnv, pwdEnv, kubeconfigEnv),
|
||||
},
|
||||
{
|
||||
name: "inheriting a subset of parent env",
|
||||
collector: &CollectHostRun{
|
||||
hostCollector: &troubleshootv1beta2.HostRun{
|
||||
Env: []string{
|
||||
"AWS_REGION=us-east-1",
|
||||
},
|
||||
InheritEnvs: []string{
|
||||
"HOME",
|
||||
},
|
||||
},
|
||||
BundlePath: "",
|
||||
},
|
||||
want: append([]string{
|
||||
"AWS_REGION=us-east-1",
|
||||
}, pathEnv, pwdEnv, homeEnv, kubeconfigEnv),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
cmd := exec.Command(test.collector.hostCollector.Command, test.collector.hostCollector.Args...)
|
||||
err := test.collector.processEnvVars(cmd)
|
||||
require.NoError(t, err)
|
||||
require.ElementsMatch(t, test.want, cmd.Env)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectHostRun_Collect(t *testing.T) {
|
||||
testDir, mkdirErr := os.MkdirTemp("", "host-run-test-*")
|
||||
defer os.RemoveAll(testDir)
|
||||
require.NoError(t, mkdirErr)
|
||||
tests := []struct {
|
||||
name string
|
||||
collector *CollectHostRun
|
||||
parentEnv []string
|
||||
want map[string][]byte
|
||||
wantError bool
|
||||
}{
|
||||
{
|
||||
name: "saving cmd run output",
|
||||
collector: &CollectHostRun{
|
||||
hostCollector: &troubleshootv1beta2.HostRun{
|
||||
HostCollectorMeta: troubleshootv1beta2.HostCollectorMeta{
|
||||
CollectorName: "my-cmd-with-output",
|
||||
},
|
||||
Command: "sh",
|
||||
Args: []string{"-c", "echo ${TS_INPUT_DIR}/dummy.conf > $TS_OUTPUT_DIR/input-file.txt"},
|
||||
Input: map[string]string{
|
||||
"dummy.conf": "[hello]\nhello = 1",
|
||||
},
|
||||
OutputDir: "magic-output",
|
||||
},
|
||||
BundlePath: path.Join(testDir, "bundle-1"),
|
||||
},
|
||||
want: CollectorResult{
|
||||
"host-collectors/run-host/my-cmd-with-output-info.json": nil,
|
||||
"host-collectors/run-host/my-cmd-with-output.txt": nil,
|
||||
"host-collectors/run-host/my-cmd-with-output/magic-output/input-file.txt": nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid input filename",
|
||||
collector: &CollectHostRun{
|
||||
hostCollector: &troubleshootv1beta2.HostRun{
|
||||
HostCollectorMeta: troubleshootv1beta2.HostCollectorMeta{
|
||||
CollectorName: "test",
|
||||
},
|
||||
Input: map[string]string{
|
||||
"/home/ec2-user/invalid-path.conf": "[hello]\nhello = 1",
|
||||
"valid-path.conf": "[env]\ndummy = dummy",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
got, err := test.collector.Collect(nil)
|
||||
if test.wantError {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, test.want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
+50
-55
@@ -4,10 +4,11 @@ import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
@@ -25,16 +26,6 @@ type HTTPError struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
var (
|
||||
httpInsecureClient = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
type CollectHTTP struct {
|
||||
Collector *troubleshootv1beta2.HTTP
|
||||
BundlePath string
|
||||
@@ -56,13 +47,20 @@ func (c *CollectHTTP) Collect(progressChan chan<- interface{}) (CollectorResult,
|
||||
var response *http.Response
|
||||
var err error
|
||||
|
||||
if c.Collector.Get != nil {
|
||||
response, err = doGet(c.Collector.Get)
|
||||
} else if c.Collector.Post != nil {
|
||||
response, err = doPost(c.Collector.Post)
|
||||
} else if c.Collector.Put != nil {
|
||||
response, err = doPut(c.Collector.Put)
|
||||
} else {
|
||||
switch {
|
||||
case c.Collector.Get != nil:
|
||||
response, err = doRequest(
|
||||
"GET", c.Collector.Get.URL, c.Collector.Get.Headers,
|
||||
"", c.Collector.Get.InsecureSkipVerify, c.Collector.Get.Timeout)
|
||||
case c.Collector.Post != nil:
|
||||
response, err = doRequest(
|
||||
"POST", c.Collector.Post.URL, c.Collector.Post.Headers,
|
||||
c.Collector.Post.Body, c.Collector.Post.InsecureSkipVerify, c.Collector.Post.Timeout)
|
||||
case c.Collector.Put != nil:
|
||||
response, err = doRequest(
|
||||
"PUT", c.Collector.Put.URL, c.Collector.Put.Headers,
|
||||
c.Collector.Put.Body, c.Collector.Put.InsecureSkipVerify, c.Collector.Put.Timeout)
|
||||
default:
|
||||
return nil, errors.New("no supported http request type")
|
||||
}
|
||||
|
||||
@@ -82,54 +80,30 @@ func (c *CollectHTTP) Collect(progressChan chan<- interface{}) (CollectorResult,
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func doGet(get *troubleshootv1beta2.Get) (*http.Response, error) {
|
||||
httpClient := http.DefaultClient
|
||||
if get.InsecureSkipVerify {
|
||||
httpClient = httpInsecureClient
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", get.URL, nil)
|
||||
func doRequest(method, url string, headers map[string]string, body string, insecureSkipVerify bool, timeout string) (*http.Response, error) {
|
||||
t, err := parseTimeout(timeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for k, v := range get.Headers {
|
||||
req.Header.Set(k, v)
|
||||
httpClient := &http.Client{
|
||||
Timeout: t,
|
||||
}
|
||||
|
||||
return httpClient.Do(req)
|
||||
}
|
||||
|
||||
func doPost(post *troubleshootv1beta2.Post) (*http.Response, error) {
|
||||
httpClient := http.DefaultClient
|
||||
if post.InsecureSkipVerify {
|
||||
httpClient = httpInsecureClient
|
||||
if insecureSkipVerify {
|
||||
httpClient.Transport = &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", post.URL, strings.NewReader(post.Body))
|
||||
req, err := http.NewRequest(method, url, strings.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for k, v := range post.Headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
|
||||
return httpClient.Do(req)
|
||||
}
|
||||
|
||||
func doPut(put *troubleshootv1beta2.Put) (*http.Response, error) {
|
||||
httpClient := http.DefaultClient
|
||||
if put.InsecureSkipVerify {
|
||||
httpClient = httpInsecureClient
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("PUT", put.URL, strings.NewReader(put.Body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for k, v := range put.Headers {
|
||||
for k, v := range headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
|
||||
@@ -143,7 +117,7 @@ func responseToOutput(response *http.Response, err error) ([]byte, error) {
|
||||
Message: err.Error(),
|
||||
}
|
||||
} else {
|
||||
body, err := ioutil.ReadAll(response.Body)
|
||||
body, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -167,3 +141,24 @@ func responseToOutput(response *http.Response, err error) ([]byte, error) {
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// parseTimeout parses a string into a time.Duration.
|
||||
// If the string is empty, it returns 0.
|
||||
func parseTimeout(s string) (time.Duration, error) {
|
||||
var timeout time.Duration
|
||||
var err error
|
||||
if s == "" {
|
||||
timeout = 0
|
||||
} else {
|
||||
timeout, err = time.ParseDuration(s)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
if timeout < 0 {
|
||||
return 0, errors.New("timeout must be a positive duration")
|
||||
}
|
||||
|
||||
return timeout, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,378 @@
|
||||
package collect
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type Headers struct {
|
||||
ContentLength string `json:"Content-Length"`
|
||||
ContentType string `json:"Content-Type"`
|
||||
Date string `json:"Date,omitempty"`
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
Status int `json:"status"`
|
||||
Body string `json:"body"`
|
||||
Headers Headers `json:"headers"`
|
||||
}
|
||||
|
||||
type ResponseData struct {
|
||||
Response Response `json:"response"`
|
||||
}
|
||||
|
||||
type args struct {
|
||||
progressChan chan<- interface{}
|
||||
}
|
||||
|
||||
type CollectorTest struct {
|
||||
name string
|
||||
httpServer *http.Server
|
||||
isHttps bool
|
||||
Collector *troubleshootv1beta2.HTTP
|
||||
args args
|
||||
checkTimeout bool
|
||||
want CollectorResult
|
||||
wantErr bool
|
||||
}
|
||||
|
||||
type ErrorResponse struct {
|
||||
Error HTTPError `json:"error"`
|
||||
}
|
||||
|
||||
func (r ResponseData) ToJSONbytes() ([]byte, error) {
|
||||
return json.Marshal(r)
|
||||
}
|
||||
|
||||
func (r ErrorResponse) ToJSONbytes() ([]byte, error) {
|
||||
return json.Marshal(r)
|
||||
}
|
||||
|
||||
func TestCollectHTTP_Collect(t *testing.T) {
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/get", func(res http.ResponseWriter, req *http.Request) {
|
||||
res.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
res.WriteHeader(http.StatusOK)
|
||||
res.Write([]byte("{\"status\": \"healthy\"}"))
|
||||
})
|
||||
mux.HandleFunc("/post", func(res http.ResponseWriter, req *http.Request) {
|
||||
res.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
res.WriteHeader(http.StatusOK)
|
||||
res.Write([]byte("Hello, POST!"))
|
||||
})
|
||||
mux.HandleFunc("/put", func(res http.ResponseWriter, req *http.Request) {
|
||||
res.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
res.WriteHeader(http.StatusOK)
|
||||
res.Write([]byte("Hello, PUT!"))
|
||||
})
|
||||
mux.HandleFunc("/error", func(res http.ResponseWriter, req *http.Request) {
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
fmt.Println("Sleeping for 2 seconds on /error call")
|
||||
res.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
res.WriteHeader(http.StatusInternalServerError)
|
||||
res.Write([]byte("{\"error\": { \"message\": \"context deadline exceeded\"}}"))
|
||||
})
|
||||
|
||||
sample_get_response := &ResponseData{
|
||||
Response: Response{
|
||||
Status: 200,
|
||||
Body: "{\"status\": \"healthy\"}",
|
||||
Headers: Headers{
|
||||
ContentLength: "21",
|
||||
ContentType: "application/json; charset=utf-8",
|
||||
},
|
||||
},
|
||||
}
|
||||
sample_get_bytes, _ := sample_get_response.ToJSONbytes()
|
||||
|
||||
sample_post_response := &ResponseData{
|
||||
Response: Response{
|
||||
Status: 200,
|
||||
Body: "Hello, POST!",
|
||||
Headers: Headers{
|
||||
ContentLength: "12",
|
||||
ContentType: "text/plain; charset=utf-8",
|
||||
},
|
||||
},
|
||||
}
|
||||
sample_post_bytes, _ := sample_post_response.ToJSONbytes()
|
||||
|
||||
sample_put_response := &ResponseData{
|
||||
Response: Response{
|
||||
Status: 200,
|
||||
Body: "Hello, PUT!",
|
||||
Headers: Headers{
|
||||
ContentLength: "11",
|
||||
ContentType: "text/plain; charset=utf-8",
|
||||
},
|
||||
},
|
||||
}
|
||||
sample_put_bytes, _ := sample_put_response.ToJSONbytes()
|
||||
|
||||
sample_error_response := &ErrorResponse{
|
||||
Error: HTTPError{
|
||||
Message: "context deadline exceeded",
|
||||
},
|
||||
}
|
||||
|
||||
sample_error_bytes, _ := sample_error_response.ToJSONbytes()
|
||||
|
||||
tests := []CollectorTest{
|
||||
{
|
||||
// check valid file path when CollectorName is not supplied
|
||||
name: "GET: collector name unset",
|
||||
Collector: &troubleshootv1beta2.HTTP{
|
||||
CollectorMeta: troubleshootv1beta2.CollectorMeta{
|
||||
CollectorName: "",
|
||||
},
|
||||
Get: &troubleshootv1beta2.Get{},
|
||||
},
|
||||
args: args{
|
||||
progressChan: nil,
|
||||
},
|
||||
want: CollectorResult{
|
||||
"result.json": sample_get_bytes,
|
||||
},
|
||||
checkTimeout: false,
|
||||
wantErr: false,
|
||||
isHttps: false,
|
||||
},
|
||||
{
|
||||
// check valid file path when CollectorName is supplied
|
||||
name: "GET: valid collect",
|
||||
Collector: &troubleshootv1beta2.HTTP{
|
||||
CollectorMeta: troubleshootv1beta2.CollectorMeta{
|
||||
CollectorName: "example-com",
|
||||
},
|
||||
Get: &troubleshootv1beta2.Get{},
|
||||
},
|
||||
args: args{
|
||||
progressChan: nil,
|
||||
},
|
||||
want: CollectorResult{
|
||||
"example-com.json": sample_get_bytes,
|
||||
},
|
||||
checkTimeout: false,
|
||||
wantErr: false,
|
||||
isHttps: false,
|
||||
},
|
||||
{
|
||||
// check valid file path when CollectorName is supplied
|
||||
name: "POST: valid collect",
|
||||
Collector: &troubleshootv1beta2.HTTP{
|
||||
CollectorMeta: troubleshootv1beta2.CollectorMeta{
|
||||
CollectorName: "example-com",
|
||||
},
|
||||
Post: &troubleshootv1beta2.Post{
|
||||
InsecureSkipVerify: true,
|
||||
Body: `{"id": 123, "name": "John Doe"}`,
|
||||
Headers: map[string]string{"Content-Type": "application/json"},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
progressChan: nil,
|
||||
},
|
||||
want: CollectorResult{
|
||||
"example-com.json": sample_post_bytes,
|
||||
},
|
||||
checkTimeout: false,
|
||||
wantErr: false,
|
||||
isHttps: false,
|
||||
},
|
||||
{
|
||||
// check valid file path when CollectorName is supplied
|
||||
name: "PUT: valid collect",
|
||||
Collector: &troubleshootv1beta2.HTTP{
|
||||
CollectorMeta: troubleshootv1beta2.CollectorMeta{
|
||||
CollectorName: "example-com",
|
||||
},
|
||||
Put: &troubleshootv1beta2.Put{
|
||||
Body: `{"id": 123, "name": "John Doe"}`,
|
||||
Headers: map[string]string{"Content-Type": "application/json"},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
progressChan: nil,
|
||||
},
|
||||
want: CollectorResult{
|
||||
"example-com.json": sample_put_bytes,
|
||||
},
|
||||
checkTimeout: false,
|
||||
wantErr: false,
|
||||
isHttps: false,
|
||||
},
|
||||
{
|
||||
name: "ERROR: check request timeout < server delay (exit early)",
|
||||
Collector: &troubleshootv1beta2.HTTP{
|
||||
CollectorMeta: troubleshootv1beta2.CollectorMeta{
|
||||
CollectorName: "example-com",
|
||||
},
|
||||
Get: &troubleshootv1beta2.Get{
|
||||
Timeout: "200ns",
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
progressChan: nil,
|
||||
},
|
||||
want: CollectorResult{
|
||||
"example-com.json": sample_error_bytes,
|
||||
},
|
||||
checkTimeout: true,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "GET: check request timeout > server delay",
|
||||
Collector: &troubleshootv1beta2.HTTP{
|
||||
CollectorMeta: troubleshootv1beta2.CollectorMeta{
|
||||
CollectorName: "",
|
||||
},
|
||||
Get: &troubleshootv1beta2.Get{
|
||||
Timeout: "300ms",
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
progressChan: nil,
|
||||
},
|
||||
want: CollectorResult{
|
||||
"result.json": sample_get_bytes,
|
||||
},
|
||||
checkTimeout: true,
|
||||
wantErr: false,
|
||||
},
|
||||
// TODO: add TLS cert case
|
||||
}
|
||||
for _, tt := range tests {
|
||||
var ts *httptest.Server
|
||||
if tt.isHttps {
|
||||
ts = httptest.NewTLSServer(mux)
|
||||
} else {
|
||||
ts = httptest.NewServer(mux)
|
||||
}
|
||||
url := ts.URL
|
||||
defer ts.Close()
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &CollectHTTP{
|
||||
Collector: tt.Collector,
|
||||
}
|
||||
|
||||
switch {
|
||||
case c.Collector.Get != nil:
|
||||
if tt.checkTimeout && tt.wantErr {
|
||||
c.Collector.Get.URL = fmt.Sprintf("%s%s", url, "/error")
|
||||
response_data := sample_error_response
|
||||
response_data.testCollectHTTP(t, &tt, c)
|
||||
} else {
|
||||
c.Collector.Get.URL = fmt.Sprintf("%s%s", url, "/get")
|
||||
response_data := sample_get_response
|
||||
response_data.testCollectHTTP(t, &tt, c)
|
||||
}
|
||||
case c.Collector.Post != nil:
|
||||
c.Collector.Post.URL = fmt.Sprintf("%s%s", url, "/post")
|
||||
response_data := sample_post_response
|
||||
response_data.testCollectHTTP(t, &tt, c)
|
||||
case c.Collector.Put != nil:
|
||||
c.Collector.Put.URL = fmt.Sprintf("%s%s", url, "/put")
|
||||
response_data := sample_put_response
|
||||
response_data.testCollectHTTP(t, &tt, c)
|
||||
default:
|
||||
t.Errorf("ERR: Method not supported")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (rd *ResponseData) testCollectHTTP(t *testing.T, tt *CollectorTest, c *CollectHTTP) {
|
||||
|
||||
got, err := c.Collect(tt.args.progressChan)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("CollectHTTP.Collect() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
var expected_filename string
|
||||
if c.Collector.CollectorName == "" {
|
||||
expected_filename = "result.json"
|
||||
} else {
|
||||
expected_filename = c.Collector.CollectorName + ".json"
|
||||
}
|
||||
|
||||
var resp ResponseData
|
||||
if err := json.Unmarshal(got[expected_filename], &resp); err != nil {
|
||||
t.Errorf("CollectHTTP.Collect() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
|
||||
// Correct format of the collected data (JSON data)
|
||||
assert.Equal(t, rd.Response.Status, resp.Response.Status)
|
||||
assert.Equal(t, rd.Response.Body, resp.Response.Body)
|
||||
assert.Equal(t, rd.Response.Headers.ContentLength, resp.Response.Headers.ContentLength)
|
||||
}
|
||||
|
||||
func (er *ErrorResponse) testCollectHTTP(t *testing.T, tt *CollectorTest, c *CollectHTTP) {
|
||||
|
||||
got, err := c.Collect(tt.args.progressChan)
|
||||
if err != nil {
|
||||
t.Errorf("CollectHTTP.Collect() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
var expected_filename string
|
||||
if c.Collector.CollectorName == "" {
|
||||
expected_filename = "result.json"
|
||||
} else {
|
||||
expected_filename = c.Collector.CollectorName + ".json"
|
||||
}
|
||||
|
||||
// assert er.Error.Message is not nil
|
||||
assert.NotNil(t, er.Error.Message)
|
||||
|
||||
if err := json.Unmarshal(got[expected_filename], &er); err != nil {
|
||||
t.Errorf("CollectHTTP.Collect() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
|
||||
if strings.Contains(strings.TrimSpace(er.Error.Message), "context deadline exceeded") != tt.wantErr {
|
||||
t.Errorf("CollectHTTP.Collect() response = %v, wantErr %v", er.Error.Message, tt.wantErr)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_parseTimeout(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want time.Duration
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "1s timeout",
|
||||
input: "1s",
|
||||
want: time.Second,
|
||||
},
|
||||
{
|
||||
name: "empty timeout",
|
||||
input: "",
|
||||
want: 0,
|
||||
},
|
||||
{
|
||||
name: "negative timeout",
|
||||
input: "-1s",
|
||||
want: 0,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := parseTimeout(tt.input)
|
||||
assert.Equal(t, (err != nil), tt.wantErr)
|
||||
assert.Equal(t, got, tt.want)
|
||||
})
|
||||
}
|
||||
}
|
||||
+19
-2
@@ -6,6 +6,7 @@ import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"regexp"
|
||||
|
||||
_ "github.com/microsoft/go-mssqldb"
|
||||
@@ -13,6 +14,7 @@ import (
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
type CollectMssql struct {
|
||||
@@ -36,14 +38,26 @@ func (c *CollectMssql) IsExcluded() (bool, error) {
|
||||
func (c *CollectMssql) Collect(progressChan chan<- interface{}) (CollectorResult, error) {
|
||||
databaseConnection := DatabaseConnection{}
|
||||
|
||||
connUrl, err := url.Parse(c.Collector.URI)
|
||||
// Parsing the uri should not lead to the collector failing
|
||||
// sql.Open will fail if the uri is invalid.
|
||||
if err == nil {
|
||||
klog.V(2).Infof("Connect to %q MSSQL Database", connUrl.Host)
|
||||
} else {
|
||||
klog.V(2).Info("Connect to MSSQL Database")
|
||||
}
|
||||
|
||||
db, err := sql.Open("mssql", c.Collector.URI)
|
||||
if err != nil {
|
||||
klog.V(2).Infof("Failed to connect to %q MSSQL Database: %w", connUrl.Host, err)
|
||||
databaseConnection.Error = err.Error()
|
||||
} else {
|
||||
defer db.Close()
|
||||
query := `select @@VERSION as version`
|
||||
row := db.QueryRow(query)
|
||||
version := ""
|
||||
if err := row.Scan(&version); err != nil {
|
||||
klog.V(2).Infof("Failed to query version string from database: %s", err)
|
||||
databaseConnection.Error = err.Error()
|
||||
} else {
|
||||
databaseConnection.IsConnected = true
|
||||
@@ -55,6 +69,7 @@ func (c *CollectMssql) Collect(progressChan chan<- interface{}) (CollectorResult
|
||||
} else {
|
||||
databaseConnection.Version = mssqlVersion
|
||||
}
|
||||
klog.V(2).Infof("Successfully queried version string from database: %s", version)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,12 +90,14 @@ func (c *CollectMssql) Collect(progressChan chan<- interface{}) (CollectorResult
|
||||
}
|
||||
|
||||
func parseMsSqlVersion(mssqlVersion string) (string, error) {
|
||||
re := regexp.MustCompile(".*SQL.*-\\s+([0-9.]+)")
|
||||
re, err := regexp.Compile(`.*SQL.*-\s+([0-9.]+)`)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to compile regex")
|
||||
}
|
||||
matches := re.FindStringSubmatch(mssqlVersion)
|
||||
if len(matches) < 2 {
|
||||
return "", errors.Errorf("mssql version did not match regex: %q", mssqlVersion)
|
||||
}
|
||||
|
||||
return matches[1], nil
|
||||
|
||||
}
|
||||
|
||||
+67
-8
@@ -5,6 +5,8 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
@@ -12,6 +14,7 @@ import (
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
type CollectPostgres struct {
|
||||
@@ -37,21 +40,75 @@ func (c *CollectPostgres) createConnectConfig() (*pgx.ConnConfig, error) {
|
||||
return nil, errors.New("postgres uri cannot be empty")
|
||||
}
|
||||
|
||||
cfg, err := pgx.ParseConfig(c.Collector.URI)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to parse postgres config")
|
||||
}
|
||||
|
||||
if c.Collector.TLS != nil {
|
||||
tlsCfg, err := createTLSConfig(c.Context, c.Client, c.Collector.TLS)
|
||||
klog.V(2).Infof("Connecting to postgres with TLS client config")
|
||||
// Set the libpq TLS environment variables since pgx parses them to
|
||||
// create the TLS configuration (tls.Config instance) to connect with
|
||||
// https://www.postgresql.org/docs/current/libpq-envars.html
|
||||
caCert, clientCert, clientKey, err := getTLSParamTriplet(c.Context, c.Client, c.Collector.TLS)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tlsCfg.ServerName = cfg.Host
|
||||
cfg.TLSConfig = tlsCfg
|
||||
// Drop the TLS params to files and set the paths to their
|
||||
// respective environment variables
|
||||
// The environment variables are unset after the connection config
|
||||
// is created. Their respective files are deleted as well.
|
||||
tmpdir, err := os.MkdirTemp("", "ts-postgres-collector")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create temp dir to store postgres collector TLS files")
|
||||
}
|
||||
defer os.RemoveAll(tmpdir)
|
||||
|
||||
if caCert != "" {
|
||||
caCertPath := filepath.Join(tmpdir, "ca.crt")
|
||||
err = os.WriteFile(caCertPath, []byte(caCert), 0644)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to write ca cert to file")
|
||||
}
|
||||
err = os.Setenv("PGSSLROOTCERT", caCertPath)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to set PGSSLROOTCERT environment variable")
|
||||
}
|
||||
klog.V(2).Infof("'PGSSLROOTCERT' environment variable set to %q", caCertPath)
|
||||
defer os.Unsetenv("PGSSLROOTCERT")
|
||||
}
|
||||
|
||||
if clientCert != "" {
|
||||
clientCertPath := filepath.Join(tmpdir, "client.crt")
|
||||
err = os.WriteFile(clientCertPath, []byte(clientCert), 0644)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to write client cert to file")
|
||||
}
|
||||
err = os.Setenv("PGSSLCERT", clientCertPath)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to set PGSSLCERT environment variable")
|
||||
}
|
||||
klog.V(2).Infof("'PGSSLCERT' environment variable set to %q", clientCertPath)
|
||||
defer os.Unsetenv("PGSSLCERT")
|
||||
}
|
||||
|
||||
if clientKey != "" {
|
||||
clientKeyPath := filepath.Join(tmpdir, "client.key")
|
||||
err = os.WriteFile(clientKeyPath, []byte(clientKey), 0600)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to write client key to file")
|
||||
}
|
||||
err = os.Setenv("PGSSLKEY", clientKeyPath)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to set PGSSLKEY environment variable")
|
||||
}
|
||||
klog.V(2).Infof("'PGSSLKEY' environment variable set to %q", clientKeyPath)
|
||||
defer os.Unsetenv("PGSSLKEY")
|
||||
}
|
||||
}
|
||||
|
||||
cfg, err := pgx.ParseConfig(c.Collector.URI)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to parse postgres config")
|
||||
}
|
||||
klog.V(2).Infof("Successfully parsed postgres config")
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
@@ -74,8 +131,10 @@ func (c *CollectPostgres) Collect(progressChan chan<- interface{}) (CollectorRes
|
||||
|
||||
conn, err := c.connect()
|
||||
if err != nil {
|
||||
klog.V(2).Infof("Postgres connection error: %s", err.Error())
|
||||
databaseConnection.Error = err.Error()
|
||||
} else {
|
||||
klog.V(2).Info("Successfully connected to postgres")
|
||||
defer conn.Close(c.Context)
|
||||
|
||||
query := `select version()`
|
||||
|
||||
@@ -2,6 +2,9 @@ package collect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"testing"
|
||||
|
||||
"github.com/replicatedhq/troubleshoot/internal/testutils"
|
||||
@@ -100,7 +103,7 @@ func TestCollectPostgres_createConnectConfigTLS(t *testing.T) {
|
||||
Client: k8sClient,
|
||||
Context: context.Background(),
|
||||
Collector: &v1beta2.Database{
|
||||
URI: "postgresql://user:password@my-pghost:5432/defaultdb?sslmode=require",
|
||||
URI: "postgresql://user:password@my-pghost:5432/defaultdb?sslmode=verify-full",
|
||||
TLS: &v1beta2.TLSParams{
|
||||
CACert: testutils.GetTestFixture(t, "db/ca.pem"),
|
||||
ClientCert: testutils.GetTestFixture(t, "db/client.pem"),
|
||||
@@ -113,7 +116,21 @@ func TestCollectPostgres_createConnectConfigTLS(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, connCfg)
|
||||
assert.Equal(t, connCfg.Host, "my-pghost")
|
||||
assert.NotNil(t, connCfg.TLSConfig.Certificates)
|
||||
|
||||
// Check client cert
|
||||
require.Len(t, connCfg.TLSConfig.Certificates, 1)
|
||||
require.Len(t, connCfg.TLSConfig.Certificates[0].Certificate, 1)
|
||||
cert := connCfg.TLSConfig.Certificates[0]
|
||||
clientCert, err := x509.ParseCertificate(cert.Certificate[0])
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "CN=client,L=Didcot,ST=Oxfordshire,C=UK", clientCert.Subject.String())
|
||||
|
||||
// Check client key
|
||||
block, _ := pem.Decode([]byte(testutils.GetTestFixture(t, "db/client-key.pem")))
|
||||
key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, key.Equal(cert.PrivateKey.(*rsa.PrivateKey)))
|
||||
|
||||
assert.NotNil(t, connCfg.TLSConfig.RootCAs)
|
||||
assert.False(t, connCfg.TLSConfig.InsecureSkipVerify)
|
||||
}
|
||||
|
||||
+12
-2
@@ -5,7 +5,6 @@ import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -17,18 +16,29 @@ import (
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
// Max number of concurrent redactors to run
|
||||
// Ensure the number is low enough since each of the redactors
|
||||
// also spawns goroutines to redact files in tar archives and
|
||||
// other goroutines for each redactor spec.
|
||||
const MAX_CONCURRENT_REDACTORS = 10
|
||||
|
||||
func RedactResult(bundlePath string, input CollectorResult, additionalRedactors []*troubleshootv1beta2.Redact) error {
|
||||
wg := &sync.WaitGroup{}
|
||||
|
||||
// Error channel to capture errors from goroutines
|
||||
errorCh := make(chan error, len(input))
|
||||
limitCh := make(chan struct{}, MAX_CONCURRENT_REDACTORS)
|
||||
defer close(limitCh)
|
||||
|
||||
for k, v := range input {
|
||||
limitCh <- struct{}{}
|
||||
|
||||
wg.Add(1)
|
||||
|
||||
go func(file string, data []byte) {
|
||||
defer wg.Done()
|
||||
defer func() { <-limitCh }() // free up after the function execution has run
|
||||
|
||||
var reader io.Reader
|
||||
if data == nil {
|
||||
|
||||
@@ -84,7 +94,7 @@ func RedactResult(bundlePath string, input CollectorResult, additionalRedactors
|
||||
// If the file is .tar, .tgz or .tar.gz, it must not be redacted. Instead it is
|
||||
// decompressed and each file inside the tar redacted and compressed back into the archive.
|
||||
if filepath.Ext(file) == ".tar" || filepath.Ext(file) == ".tgz" || strings.HasSuffix(file, ".tar.gz") {
|
||||
tmpDir, err := ioutil.TempDir("", "troubleshoot-subresult-")
|
||||
tmpDir, err := os.MkdirTemp("", "troubleshoot-subresult-")
|
||||
if err != nil {
|
||||
errorCh <- errors.Wrap(err, "failed to create temp dir")
|
||||
return
|
||||
|
||||
+42
-1
@@ -4,9 +4,12 @@ import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"k8s.io/klog/v2"
|
||||
@@ -132,7 +135,45 @@ func (r CollectorResult) SaveResult(bundlePath string, relativePath string, read
|
||||
return errors.Wrap(err, "failed to stat file")
|
||||
}
|
||||
|
||||
klog.V(2).Infof("Added %q (%d MB) to bundle output", relativePath, fileInfo.Size()/(1024*1024))
|
||||
klog.V(2).Infof("Added %q (%d KB) to bundle output", relativePath, fileInfo.Size()/(1024))
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveResults walk a target directory and call SaveResult on all files retrieved from the walk.
|
||||
func (r CollectorResult) SaveResults(bundlePath, relativePath, targetDir string) error {
|
||||
dirPath := path.Join(bundlePath, relativePath)
|
||||
if err := os.MkdirAll(dirPath, 0777); err != nil {
|
||||
return errors.Wrap(err, "failed to create output file directory")
|
||||
}
|
||||
|
||||
err := filepath.WalkDir(targetDir, func(path string, d os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error from WalkDirFunc")
|
||||
}
|
||||
|
||||
if !d.IsDir() {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("failed to open file: %s", path))
|
||||
}
|
||||
fileBytes, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("failed to read file: %s", path))
|
||||
}
|
||||
bundleRelativePath := filepath.Join(relativePath, strings.TrimPrefix(path, targetDir+"/"))
|
||||
err = r.SaveResult(bundlePath, bundleRelativePath, bytes.NewBuffer(fileBytes))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error from SaveResult call")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error from WalkDir call")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+23
-11
@@ -139,12 +139,30 @@ func listNodesInSelector(ctx context.Context, client *kubernetes.Clientset, sele
|
||||
|
||||
nodes, err := client.CoreV1().Nodes().List(ctx, listOptions)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Can't get the list of nodes, got: %w", err)
|
||||
return nil, fmt.Errorf("can't get the list of nodes, got: %w", err)
|
||||
}
|
||||
|
||||
return nodes.Items, nil
|
||||
}
|
||||
|
||||
func getTLSParamTriplet(
|
||||
ctx context.Context, client kubernetes.Interface, params *troubleshootv1beta2.TLSParams,
|
||||
) (string, string, string, error) {
|
||||
var caCert, clientCert, clientKey string
|
||||
if params.Secret != nil {
|
||||
var err error
|
||||
caCert, clientCert, clientKey, err = getTLSParamsFromSecret(ctx, client, params.Secret)
|
||||
if err != nil {
|
||||
return caCert, clientCert, clientKey, err
|
||||
}
|
||||
} else {
|
||||
caCert = params.CACert
|
||||
clientCert = params.ClientCert
|
||||
clientKey = params.ClientKey
|
||||
}
|
||||
return caCert, clientCert, clientKey, nil
|
||||
}
|
||||
|
||||
func createTLSConfig(ctx context.Context, client kubernetes.Interface, params *troubleshootv1beta2.TLSParams) (*tls.Config, error) {
|
||||
rootCA, err := x509.SystemCertPool()
|
||||
if err != nil {
|
||||
@@ -158,21 +176,15 @@ func createTLSConfig(ctx context.Context, client kubernetes.Interface, params *t
|
||||
return tlsCfg, nil
|
||||
}
|
||||
|
||||
var caCert, clientCert, clientKey string
|
||||
if params.Secret != nil {
|
||||
caCert, clientCert, clientKey, err = getTLSParamsFromSecret(ctx, client, params.Secret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
caCert = params.CACert
|
||||
clientCert = params.ClientCert
|
||||
clientKey = params.ClientKey
|
||||
caCert, clientCert, clientKey, err := getTLSParamTriplet(ctx, client, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ok := rootCA.AppendCertsFromPEM([]byte(caCert)); !ok {
|
||||
return nil, fmt.Errorf("failed to append CA cert to root CA bundle")
|
||||
}
|
||||
|
||||
tlsCfg.RootCAs = rootCA
|
||||
|
||||
if clientCert == "" && clientKey == "" {
|
||||
|
||||
@@ -70,11 +70,12 @@ const (
|
||||
EXIT_CODE_WARN = 4
|
||||
|
||||
// Troubleshoot label constants
|
||||
SupportBundleKey = "support-bundle-spec"
|
||||
RedactorKey = "redactor-spec"
|
||||
TroubleshootIOLabelKey = "troubleshoot.io/kind"
|
||||
TroubleshootSHLabelKey = "troubleshoot.sh/kind"
|
||||
PreflightKey = "preflight.yaml" // Shouldn't this be "preflight-spec"?
|
||||
SupportBundleKey = "support-bundle-spec"
|
||||
RedactorKey = "redactor-spec"
|
||||
PreflightKey = "preflight.yaml"
|
||||
PreflightKey2 = "preflight-spec"
|
||||
|
||||
// Troubleshoot spec constants
|
||||
Troubleshootv1beta2Kind = "troubleshoot.sh/v1beta2"
|
||||
@@ -83,6 +84,11 @@ const (
|
||||
MESSAGE_TEXT_PADDING = 4
|
||||
MESSAGE_TEXT_LINES_MARGIN_TO_BOTTOM = 4
|
||||
|
||||
// Bufio Reader Constants
|
||||
MAX_BUFFER_CAPACITY = 1024 * 1024
|
||||
// This is the initial size of the buffer allocated.
|
||||
// Under the hood, an array of size N is allocated in memory
|
||||
BUF_INIT_SIZE = 4096 // 4KB
|
||||
|
||||
// This is the muximum size the buffer can grow to
|
||||
// Its not what the buffer will be allocated to initially
|
||||
SCANNER_MAX_SIZE = 10 * 1024 * 1024 // 10MB
|
||||
)
|
||||
|
||||
+75
-7
@@ -2,6 +2,8 @@ package loader
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/replicatedhq/troubleshoot/internal/util"
|
||||
@@ -10,10 +12,10 @@ import (
|
||||
"github.com/replicatedhq/troubleshoot/pkg/constants"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/docrewrite"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/types"
|
||||
"gopkg.in/yaml.v2"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/klog/v2"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
var decoder runtime.Decoder
|
||||
@@ -40,6 +42,14 @@ type LoadOptions struct {
|
||||
Strict bool
|
||||
}
|
||||
|
||||
// TODO: Additional requirements needed in this package
|
||||
// * Downloading specs from remote locations e.g oci, s3, http etc
|
||||
// * Remote connection error handing
|
||||
// * Support various auth methods
|
||||
// * Retry logic and how to handle timeouts
|
||||
// * Support for loading specs from paths e.g directory, file, stdin, tarballs, zips etc
|
||||
// * Support for loading specs from a kubernetes cluster - concrete use case of remote location
|
||||
|
||||
// LoadSpecs takes sources to load specs from and returns a TroubleshootKinds object
|
||||
// that contains all the parsed troubleshoot specs.
|
||||
//
|
||||
@@ -57,11 +67,8 @@ func LoadSpecs(ctx context.Context, opt LoadOptions) (*TroubleshootKinds, error)
|
||||
l := specLoader{
|
||||
strict: opt.Strict,
|
||||
}
|
||||
return l.loadFromStrings(opt.RawSpecs...)
|
||||
}
|
||||
|
||||
type specLoader struct {
|
||||
strict bool
|
||||
return l.loadFromStrings(opt.RawSpecs...)
|
||||
}
|
||||
|
||||
type TroubleshootKinds struct {
|
||||
@@ -86,10 +93,52 @@ func (kinds *TroubleshootKinds) IsEmpty() bool {
|
||||
len(kinds.SupportBundlesV1Beta2) == 0
|
||||
}
|
||||
|
||||
func (kinds *TroubleshootKinds) Add(other *TroubleshootKinds) {
|
||||
kinds.AnalyzersV1Beta2 = append(kinds.AnalyzersV1Beta2, other.AnalyzersV1Beta2...)
|
||||
kinds.CollectorsV1Beta2 = append(kinds.CollectorsV1Beta2, other.CollectorsV1Beta2...)
|
||||
kinds.HostCollectorsV1Beta2 = append(kinds.HostCollectorsV1Beta2, other.HostCollectorsV1Beta2...)
|
||||
kinds.HostPreflightsV1Beta2 = append(kinds.HostPreflightsV1Beta2, other.HostPreflightsV1Beta2...)
|
||||
kinds.PreflightsV1Beta2 = append(kinds.PreflightsV1Beta2, other.PreflightsV1Beta2...)
|
||||
kinds.RedactorsV1Beta2 = append(kinds.RedactorsV1Beta2, other.RedactorsV1Beta2...)
|
||||
kinds.RemoteCollectorsV1Beta2 = append(kinds.RemoteCollectorsV1Beta2, other.RemoteCollectorsV1Beta2...)
|
||||
kinds.SupportBundlesV1Beta2 = append(kinds.SupportBundlesV1Beta2, other.SupportBundlesV1Beta2...)
|
||||
}
|
||||
|
||||
// ToYaml returns a yaml document/multi-doc of all the parsed specs
|
||||
// This function utilises reflection to iterate over all the fields
|
||||
// of the TroubleshootKinds object then marshals them to yaml.
|
||||
func (kinds *TroubleshootKinds) ToYaml() (string, error) {
|
||||
rawList := []string{}
|
||||
obj := reflect.ValueOf(*kinds)
|
||||
|
||||
for i := 0; i < obj.NumField(); i++ {
|
||||
field := obj.Field(i)
|
||||
if field.Kind() != reflect.Slice {
|
||||
continue
|
||||
}
|
||||
|
||||
// skip empty slices to avoid empty yaml documents
|
||||
for count := 0; count < field.Len(); count++ {
|
||||
val := field.Index(count)
|
||||
yamlOut, err := yaml.Marshal(val.Interface())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
rawList = append(rawList, string(yamlOut))
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(rawList, "---\n"), nil
|
||||
}
|
||||
|
||||
func NewTroubleshootKinds() *TroubleshootKinds {
|
||||
return &TroubleshootKinds{}
|
||||
}
|
||||
|
||||
type specLoader struct {
|
||||
strict bool
|
||||
}
|
||||
|
||||
// loadFromStrings accepts a list of strings (exploded) which should be yaml documents
|
||||
func (l *specLoader) loadFromStrings(rawSpecs ...string) (*TroubleshootKinds, error) {
|
||||
splitdocs := []string{}
|
||||
@@ -147,7 +196,7 @@ func (l *specLoader) loadFromStrings(rawSpecs ...string) (*TroubleshootKinds, er
|
||||
// If it's not a configmap or secret, just append it to the splitdocs
|
||||
splitdocs = append(splitdocs, rawDoc)
|
||||
} else {
|
||||
klog.V(1).Infof("skip loading %q kind", parsed.Kind)
|
||||
klog.V(1).Infof("Skip loading %q kind", parsed.Kind)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,7 +250,12 @@ func (l *specLoader) loadFromSplitDocs(splitdocs []string) (*TroubleshootKinds,
|
||||
}
|
||||
}
|
||||
|
||||
klog.V(1).Info("loaded troubleshoot specs successfully")
|
||||
if kinds.IsEmpty() {
|
||||
klog.V(1).Info("No troubleshoot specs were loaded")
|
||||
} else {
|
||||
klog.V(1).Info("Loaded troubleshoot specs successfully")
|
||||
}
|
||||
|
||||
return kinds, nil
|
||||
}
|
||||
|
||||
@@ -223,6 +277,7 @@ func isConfigMap(parsedDocHead parsedDoc) bool {
|
||||
|
||||
// getSpecFromConfigMap extracts multiple troubleshoot specs from a secret
|
||||
func (l *specLoader) getSpecFromConfigMap(cm *v1.ConfigMap) ([]string, error) {
|
||||
// TODO: Consider not checking for the existence of the key and just trying to decode
|
||||
specs := []string{}
|
||||
|
||||
str, ok := cm.Data[constants.SupportBundleKey]
|
||||
@@ -237,12 +292,17 @@ func (l *specLoader) getSpecFromConfigMap(cm *v1.ConfigMap) ([]string, error) {
|
||||
if ok {
|
||||
specs = append(specs, util.SplitYAML(str)...)
|
||||
}
|
||||
str, ok = cm.Data[constants.PreflightKey2]
|
||||
if ok {
|
||||
specs = append(specs, util.SplitYAML(str)...)
|
||||
}
|
||||
|
||||
return specs, nil
|
||||
}
|
||||
|
||||
// getSpecFromSecret extracts multiple troubleshoot specs from a secret
|
||||
func (l *specLoader) getSpecFromSecret(secret *v1.Secret) ([]string, error) {
|
||||
// TODO: Consider not checking for the existence of the key and just trying to decode
|
||||
specs := []string{}
|
||||
|
||||
specBytes, ok := secret.Data[constants.SupportBundleKey]
|
||||
@@ -257,6 +317,10 @@ func (l *specLoader) getSpecFromSecret(secret *v1.Secret) ([]string, error) {
|
||||
if ok {
|
||||
specs = append(specs, util.SplitYAML(string(specBytes))...)
|
||||
}
|
||||
specBytes, ok = secret.Data[constants.PreflightKey2]
|
||||
if ok {
|
||||
specs = append(specs, util.SplitYAML(string(specBytes))...)
|
||||
}
|
||||
str, ok := secret.StringData[constants.SupportBundleKey]
|
||||
if ok {
|
||||
specs = append(specs, util.SplitYAML(str)...)
|
||||
@@ -269,5 +333,9 @@ func (l *specLoader) getSpecFromSecret(secret *v1.Secret) ([]string, error) {
|
||||
if ok {
|
||||
specs = append(specs, util.SplitYAML(str)...)
|
||||
}
|
||||
str, ok = secret.StringData[constants.PreflightKey2]
|
||||
if ok {
|
||||
specs = append(specs, util.SplitYAML(str)...)
|
||||
}
|
||||
return specs, nil
|
||||
}
|
||||
|
||||
@@ -507,3 +507,96 @@ func TestKindsIsEmpty(t *testing.T) {
|
||||
kinds.AnalyzersV1Beta2 = append(kinds.AnalyzersV1Beta2, troubleshootv1beta2.Analyzer{})
|
||||
assert.False(t, kinds.IsEmpty())
|
||||
}
|
||||
|
||||
func TestAddingKinds(t *testing.T) {
|
||||
a := troubleshootv1beta2.Analyzer{
|
||||
Spec: troubleshootv1beta2.AnalyzerSpec{},
|
||||
}
|
||||
k1 := NewTroubleshootKinds()
|
||||
k1.Add(&TroubleshootKinds{
|
||||
AnalyzersV1Beta2: []troubleshootv1beta2.Analyzer{a},
|
||||
})
|
||||
|
||||
k2 := &TroubleshootKinds{
|
||||
AnalyzersV1Beta2: []troubleshootv1beta2.Analyzer{a},
|
||||
}
|
||||
assert.Equal(t, k2, k1)
|
||||
}
|
||||
|
||||
func TestToYaml(t *testing.T) {
|
||||
k := &TroubleshootKinds{
|
||||
AnalyzersV1Beta2: []troubleshootv1beta2.Analyzer{
|
||||
{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Analyzer",
|
||||
APIVersion: "troubleshoot.sh/v1beta2",
|
||||
},
|
||||
Spec: troubleshootv1beta2.AnalyzerSpec{
|
||||
Analyzers: []*troubleshootv1beta2.Analyze{
|
||||
{
|
||||
ClusterVersion: &troubleshootv1beta2.ClusterVersion{
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
Message: "Cluster is up to date",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
SupportBundlesV1Beta2: []troubleshootv1beta2.SupportBundle{
|
||||
{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "SupportBundle",
|
||||
APIVersion: "troubleshoot.sh/v1beta2",
|
||||
},
|
||||
Spec: troubleshootv1beta2.SupportBundleSpec{
|
||||
Collectors: []*troubleshootv1beta2.Collect{
|
||||
{
|
||||
ClusterResources: &troubleshootv1beta2.ClusterResources{
|
||||
IgnoreRBAC: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
y, err := k.ToYaml()
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(y), `apiVersion: troubleshoot.sh/v1beta2
|
||||
kind: SupportBundle
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
spec:
|
||||
collectors:
|
||||
- clusterResources:
|
||||
ignoreRBAC: true`)
|
||||
assert.Contains(t, string(y), "message: Cluster is up to date")
|
||||
}
|
||||
|
||||
func TestLoadingEmptySpec(t *testing.T) {
|
||||
s := testutils.GetTestFixture(t, "supportbundle/empty.yaml")
|
||||
kinds, err := LoadSpecs(context.Background(), LoadOptions{RawSpec: s})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, kinds)
|
||||
|
||||
assert.Equal(t, &TroubleshootKinds{
|
||||
SupportBundlesV1Beta2: []troubleshootv1beta2.SupportBundle{
|
||||
{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "SupportBundle",
|
||||
APIVersion: "troubleshoot.sh/v1beta2",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "empty",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, kinds)
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ package logger
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
@@ -45,6 +46,25 @@ func InitKlogFlags(cmd *cobra.Command) {
|
||||
})
|
||||
}
|
||||
|
||||
// InitKlog initializes klog with a specific verbosity. This is useful when we want to
|
||||
// use klog in a library and we want to control the verbosity from the library's caller.
|
||||
// We can use this in tests to print instrumented logs for example.
|
||||
func InitKlog(verbosity int) {
|
||||
// Initialize klog flags
|
||||
klogFlags := flag.NewFlagSet("klog", flag.ExitOnError)
|
||||
klog.InitFlags(klogFlags)
|
||||
|
||||
klogFlags.VisitAll(func(f *flag.Flag) {
|
||||
// Just the flags we want to expose in our CLI
|
||||
if f.Name == "v" {
|
||||
// If we ever want to expose the klog flags that have underscores ("_") in them
|
||||
// we need to replace them with hyphens ("-") in the flag name using
|
||||
// pflag.NormalizedName(strings.ReplaceAll(name, "_", "-")). Check how kubectl does it
|
||||
f.Value.Set(fmt.Sprintf("%d", verbosity))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// SetupLogger sets up klog logger based on viper configuration.
|
||||
func SetupLogger(v *viper.Viper) {
|
||||
quiet := v.GetBool("debug") || v.IsSet("v")
|
||||
|
||||
+80
-19
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
@@ -11,6 +12,7 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
"github.com/replicatedhq/troubleshoot/internal/util"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/version"
|
||||
"k8s.io/klog/v2"
|
||||
"oras.land/oras-go/pkg/auth"
|
||||
dockerauth "oras.land/oras-go/pkg/auth/docker"
|
||||
"oras.land/oras-go/pkg/content"
|
||||
@@ -27,14 +29,52 @@ var (
|
||||
)
|
||||
|
||||
func PullPreflightFromOCI(uri string) ([]byte, error) {
|
||||
return pullFromOCI(uri, "replicated.preflight.spec", "replicated-preflight")
|
||||
return pullFromOCI(context.Background(), uri, "replicated.preflight.spec", "replicated-preflight")
|
||||
}
|
||||
|
||||
func PullSupportBundleFromOCI(uri string) ([]byte, error) {
|
||||
return pullFromOCI(uri, "replicated.supportbundle.spec", "replicated-supportbundle")
|
||||
return pullFromOCI(context.Background(), uri, "replicated.supportbundle.spec", "replicated-supportbundle")
|
||||
}
|
||||
|
||||
func pullFromOCI(uri string, mediaType string, imageName string) ([]byte, error) {
|
||||
// PullSpecsFromOCI pulls both the preflight and support bundle specs from the given URI
|
||||
//
|
||||
// The URI is expected to be the same as the one used to install your KOTS application
|
||||
// Example oci://registry.replicated.com/app-slug/unstable will endup pulling
|
||||
// preflights from "registry.replicated.com/app-slug/unstable/replicated-preflight:latest"
|
||||
// and support bundles from "registry.replicated.com/app-slug/unstable/replicated-supportbundle:latest"
|
||||
// Both images have their own media types created when publishing KOTS OCI image.
|
||||
// NOTE: This only works with replicated registries for now and for KOTS applications only
|
||||
func PullSpecsFromOCI(ctx context.Context, uri string) ([]string, error) {
|
||||
// TODOs (API is opinionated, but we should be able to support these):
|
||||
// - Pulling from generic OCI registries (not just replicated)
|
||||
// - Pulling from registries that require authentication
|
||||
// - Passing in a complete URI including tags and image name
|
||||
|
||||
rawSpecs := []string{}
|
||||
|
||||
// First try to pull the preflight spec
|
||||
rawPreflight, err := pullFromOCI(ctx, uri, "replicated.preflight.spec", "replicated-preflight")
|
||||
if err != nil {
|
||||
// Ignore "not found" error and continue fetching the support bundle spec
|
||||
if !errors.Is(err, ErrNoRelease) {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
rawSpecs = append(rawSpecs, string(rawPreflight))
|
||||
}
|
||||
|
||||
// Then try to pull the support bundle spec
|
||||
rawSupportBundle, err := pullFromOCI(ctx, uri, "replicated.supportbundle.spec", "replicated-supportbundle")
|
||||
// If we had found a preflight spec, do not return an error
|
||||
if err != nil && len(rawSpecs) == 0 {
|
||||
return nil, err
|
||||
}
|
||||
rawSpecs = append(rawSpecs, string(rawSupportBundle))
|
||||
|
||||
return rawSpecs, nil
|
||||
}
|
||||
|
||||
func pullFromOCI(ctx context.Context, uri string, mediaType string, imageName string) ([]byte, error) {
|
||||
// helm credentials
|
||||
helmCredentialsFile := filepath.Join(util.HomeDir(), HelmCredentialsFileBasename)
|
||||
dockerauthClient, err := dockerauth.NewClientWithDockerFallback(helmCredentialsFile)
|
||||
@@ -60,24 +100,14 @@ func pullFromOCI(uri string, mediaType string, imageName string) ([]byte, error)
|
||||
var descriptors, layers []ocispec.Descriptor
|
||||
registryStore := content.Registry{Resolver: resolver}
|
||||
|
||||
// remove the oci://
|
||||
uri = strings.TrimPrefix(uri, "oci://")
|
||||
|
||||
uriParts := strings.Split(uri, ":")
|
||||
uri = fmt.Sprintf("%s/%s", uriParts[0], imageName)
|
||||
|
||||
if len(uriParts) > 1 {
|
||||
uri = fmt.Sprintf("%s:%s", uri, uriParts[1])
|
||||
} else {
|
||||
uri = fmt.Sprintf("%s:latest", uri)
|
||||
}
|
||||
|
||||
parsedRef, err := registry.ParseReference(uri)
|
||||
parsedRef, err := parseURI(uri, imageName)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to parse reference")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
manifest, err := oras.Copy(context.TODO(), registryStore, parsedRef.String(), memoryStore, "",
|
||||
klog.V(1).Infof("Pulling spec from %q OCI uri", parsedRef)
|
||||
|
||||
manifest, err := oras.Copy(ctx, registryStore, parsedRef, memoryStore, "",
|
||||
oras.WithPullEmptyNameAllowed(),
|
||||
oras.WithAllowedMediaTypes(allowedMediaTypes),
|
||||
oras.WithLayerDescriptors(func(l []ocispec.Descriptor) {
|
||||
@@ -94,7 +124,7 @@ func pullFromOCI(uri string, mediaType string, imageName string) ([]byte, error)
|
||||
descriptors = append(descriptors, manifest)
|
||||
descriptors = append(descriptors, layers...)
|
||||
|
||||
// expect 1 descriptor
|
||||
// expect 2 descriptors
|
||||
if len(descriptors) != 2 {
|
||||
return nil, fmt.Errorf("expected 2 descriptor, got %d", len(descriptors))
|
||||
}
|
||||
@@ -120,3 +150,34 @@ func pullFromOCI(uri string, mediaType string, imageName string) ([]byte, error)
|
||||
|
||||
return matchingSpec, nil
|
||||
}
|
||||
|
||||
func parseURI(in, imageName string) (string, error) {
|
||||
u, err := url.Parse(in)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Always check the scheme. If more schemes need to be supported
|
||||
// we need to compare u.Scheme against a list of supported schemes.
|
||||
// url.Parse(raw) will not return an error if a scheme is not present.
|
||||
if u.Scheme != "oci" {
|
||||
return "", fmt.Errorf("%q is an invalid OCI registry scheme", u.Scheme)
|
||||
}
|
||||
|
||||
// remove unnecessary bits (oci://, tags)
|
||||
uriParts := strings.Split(u.EscapedPath(), ":")
|
||||
|
||||
tag := "latest"
|
||||
if len(uriParts) > 1 {
|
||||
tag = uriParts[1]
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("%s%s/%s:%s", u.Host, uriParts[0], imageName, tag) // <host>:<port>/path/<imageName>:tag
|
||||
|
||||
parsedRef, err := registry.ParseReference(uri)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to parse OCI uri reference")
|
||||
}
|
||||
|
||||
return parsedRef.String(), nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package oci
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_parseURI(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
uri string
|
||||
imageName string
|
||||
wantUri string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "happy path",
|
||||
uri: "oci://registry.replicated.com/app-slug/unstable",
|
||||
imageName: "replicated-preflight",
|
||||
wantUri: "registry.replicated.com/app-slug/unstable/replicated-preflight:latest",
|
||||
},
|
||||
{
|
||||
name: "no path in uri",
|
||||
uri: "oci://registry.replicated.com",
|
||||
imageName: "replicated-preflight",
|
||||
wantUri: "registry.replicated.com/replicated-preflight:latest",
|
||||
},
|
||||
{
|
||||
name: "hostname with port",
|
||||
uri: "oci://localhost:5000/some/path",
|
||||
imageName: "replicated-preflight",
|
||||
wantUri: "localhost:5000/some/path/replicated-preflight:latest",
|
||||
},
|
||||
{
|
||||
name: "uri with tags",
|
||||
uri: "oci://localhost:5000/some/path:tag",
|
||||
imageName: "replicated-preflight",
|
||||
wantUri: "localhost:5000/some/path/replicated-preflight:tag",
|
||||
},
|
||||
{
|
||||
name: "empty uri",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid uri",
|
||||
uri: "registry.replicated.com/app-slug/unstable",
|
||||
imageName: "replicated-preflight",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid uri",
|
||||
uri: "https://registry.replicated.com/app-slug/unstable",
|
||||
imageName: "replicated-preflight",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := parseURI(tt.uri, tt.imageName)
|
||||
require.Equalf(t, tt.wantErr, err != nil, "parseURI() error = %v, wantErr %v", err, tt.wantErr)
|
||||
assert.Equalf(t, tt.wantUri, got, "parseURI() = %v, want %v", got, tt.wantUri)
|
||||
})
|
||||
}
|
||||
}
|
||||
+42
-107
@@ -2,125 +2,60 @@ package preflight
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/replicatedhq/troubleshoot/internal/util"
|
||||
"github.com/replicatedhq/troubleshoot/internal/specs"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/constants"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/k8sutil"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/loader"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/oci"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/specs"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/types"
|
||||
"github.com/spf13/viper"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
)
|
||||
|
||||
type PreflightSpecs struct {
|
||||
PreflightSpec *troubleshootv1beta2.Preflight
|
||||
HostPreflightSpec *troubleshootv1beta2.HostPreflight
|
||||
UploadResultSpecs []*troubleshootv1beta2.Preflight
|
||||
}
|
||||
func readSpecs(args []string) (*loader.TroubleshootKinds, error) {
|
||||
config, err := k8sutil.GetRESTConfig()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to convert kube flags to rest config")
|
||||
}
|
||||
|
||||
func (p *PreflightSpecs) Read(args []string) error {
|
||||
var preflightContent []byte
|
||||
var err error
|
||||
client, err := kubernetes.NewForConfig(config)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to convert create k8s client")
|
||||
}
|
||||
|
||||
// TODO: Earmarked for cleanup in favour of loader.LoadFromArgs(args []string)
|
||||
for _, v := range args {
|
||||
if strings.HasPrefix(v, "secret/") {
|
||||
// format secret/namespace-name/secret-name
|
||||
pathParts := strings.Split(v, "/")
|
||||
if len(pathParts) != 3 {
|
||||
return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, errors.Errorf("path %s must have 3 components", v))
|
||||
}
|
||||
ctx := context.Background()
|
||||
kinds, err := specs.LoadFromCLIArgs(ctx, client, args, viper.GetViper())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
spec, err := specs.LoadFromSecret(pathParts[1], pathParts[2], "preflight-spec")
|
||||
if err != nil {
|
||||
return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, errors.Wrap(err, "failed to get spec from secret"))
|
||||
}
|
||||
ret := loader.NewTroubleshootKinds()
|
||||
|
||||
preflightContent = spec
|
||||
} else if _, err = os.Stat(v); err == nil {
|
||||
b, err := os.ReadFile(v)
|
||||
if err != nil {
|
||||
return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, err)
|
||||
}
|
||||
|
||||
preflightContent = b
|
||||
} else if v == "-" {
|
||||
b, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
return types.NewExitCodeError(constants.EXIT_CODE_CATCH_ALL, err)
|
||||
}
|
||||
preflightContent = b
|
||||
// Concatenate all preflight inclusterSpecs that don't have an upload destination
|
||||
inclusterSpecs := []troubleshootv1beta2.Preflight{}
|
||||
var concatenatedSpec *troubleshootv1beta2.Preflight
|
||||
for _, v := range kinds.PreflightsV1Beta2 {
|
||||
v := v // https://golang.org/doc/faq#closures_and_goroutines
|
||||
if v.Spec.UploadResultsTo == "" {
|
||||
concatenatedSpec = ConcatPreflightSpec(concatenatedSpec, &v)
|
||||
} else {
|
||||
u, err := url.Parse(v)
|
||||
if err != nil {
|
||||
return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, err)
|
||||
}
|
||||
|
||||
if u.Scheme == "oci" {
|
||||
content, err := oci.PullPreflightFromOCI(v)
|
||||
if err != nil {
|
||||
if err == oci.ErrNoRelease {
|
||||
return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, errors.Errorf("no release found for %s.\nCheck the oci:// uri for errors or contact the application vendor for support.", v))
|
||||
}
|
||||
|
||||
return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, err)
|
||||
}
|
||||
|
||||
preflightContent = content
|
||||
} else {
|
||||
if !util.IsURL(v) {
|
||||
return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, fmt.Errorf("%s is not a URL and was not found (err %s)", v, err))
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", v, nil)
|
||||
if err != nil {
|
||||
// exit code: should this be catch all or spec issues...?
|
||||
return types.NewExitCodeError(constants.EXIT_CODE_CATCH_ALL, err)
|
||||
}
|
||||
req.Header.Set("User-Agent", "Replicated_Preflight/v1beta2")
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
// exit code: should this be catch all or spec issues...?
|
||||
return types.NewExitCodeError(constants.EXIT_CODE_CATCH_ALL, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, err)
|
||||
}
|
||||
|
||||
preflightContent = body
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
kinds, err := loader.LoadSpecs(ctx, loader.LoadOptions{
|
||||
RawSpec: string(preflightContent),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, v := range kinds.PreflightsV1Beta2 {
|
||||
if v.Spec.UploadResultsTo == "" {
|
||||
p.PreflightSpec = ConcatPreflightSpec(p.PreflightSpec, &v)
|
||||
} else {
|
||||
p.UploadResultSpecs = append(p.UploadResultSpecs, &v)
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range kinds.HostPreflightsV1Beta2 {
|
||||
p.HostPreflightSpec = ConcatHostPreflightSpec(p.HostPreflightSpec, &v)
|
||||
inclusterSpecs = append(inclusterSpecs, v)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
if concatenatedSpec != nil {
|
||||
inclusterSpecs = append(inclusterSpecs, *concatenatedSpec)
|
||||
}
|
||||
ret.PreflightsV1Beta2 = inclusterSpecs
|
||||
|
||||
var hostSpec *troubleshootv1beta2.HostPreflight
|
||||
for _, v := range kinds.HostPreflightsV1Beta2 {
|
||||
v := v // https://golang.org/doc/faq#closures_and_goroutines
|
||||
hostSpec = ConcatHostPreflightSpec(hostSpec, &v)
|
||||
}
|
||||
if hostSpec != nil {
|
||||
ret.HostPreflightsV1Beta2 = []troubleshootv1beta2.HostPreflight{*hostSpec}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package preflight
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/replicatedhq/troubleshoot/internal/testutils"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/loader"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -26,7 +26,7 @@ type PreflightSpecsReadTest struct {
|
||||
// TODOLATER: tests around this
|
||||
wantHostPreflightSpec *troubleshootv1beta2.HostPreflight
|
||||
// TODOLATER: tests around this
|
||||
wantUploadResultSpecs []*troubleshootv1beta2.Preflight
|
||||
wantUploadResultSpecs []troubleshootv1beta2.Preflight
|
||||
}
|
||||
|
||||
// TODO: Simplify tests and rely on the loader tests
|
||||
@@ -263,6 +263,42 @@ func TestPreflightSpecsRead(t *testing.T) {
|
||||
wantHostPreflightSpec: nil,
|
||||
wantUploadResultSpecs: nil,
|
||||
},
|
||||
PreflightSpecsReadTest{
|
||||
name: "stdin-secret and file-preflight",
|
||||
args: []string{"-", preflightFile},
|
||||
customStdin: true,
|
||||
stdinDataFile: preflightSecretFile,
|
||||
wantErr: false,
|
||||
wantPreflightSpec: concatSpecs(expectSecretPreflightSpec, expectPreflightSpec),
|
||||
wantHostPreflightSpec: nil,
|
||||
wantUploadResultSpecs: nil,
|
||||
},
|
||||
PreflightSpecsReadTest{
|
||||
name: "stdin-secret and support-bundle secret",
|
||||
args: []string{
|
||||
"-",
|
||||
filepath.Join(testutils.FileDir(), "../../testdata/supportbundle/labelled-specs/sb-spec-1.yaml"),
|
||||
},
|
||||
customStdin: true,
|
||||
stdinDataFile: preflightSecretFile,
|
||||
wantErr: false,
|
||||
wantPreflightSpec: &expectSecretPreflightSpec,
|
||||
wantHostPreflightSpec: nil,
|
||||
wantUploadResultSpecs: nil,
|
||||
},
|
||||
PreflightSpecsReadTest{
|
||||
name: "stdin-secret and redact secret",
|
||||
args: []string{
|
||||
"-",
|
||||
filepath.Join(testutils.FileDir(), "../../testdata/supportbundle/labelled-specs/redact-spec-1.yaml"),
|
||||
},
|
||||
customStdin: true,
|
||||
stdinDataFile: preflightSecretFile,
|
||||
wantErr: false,
|
||||
wantPreflightSpec: &expectSecretPreflightSpec,
|
||||
wantHostPreflightSpec: nil,
|
||||
wantUploadResultSpecs: nil,
|
||||
},
|
||||
/*
|
||||
/* TODOLATER: needs a cluster with a spec installed?
|
||||
PreflightSpecsReadTest{
|
||||
@@ -279,25 +315,60 @@ func TestPreflightSpecsRead(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
specs := PreflightSpecs{}
|
||||
tt := tt // pin
|
||||
|
||||
tErr := singleTestPreflightSpecsRead(t, &tt, &specs)
|
||||
specs, tErr := singleTestPreflightSpecsRead(t, &tt)
|
||||
require.Equal(t, tt.wantErr, tErr != nil)
|
||||
|
||||
if tt.wantErr {
|
||||
assert.Error(t, tErr)
|
||||
} else {
|
||||
require.NoError(t, tErr)
|
||||
if tt.wantPreflightSpec != nil {
|
||||
assert.Truef(t,
|
||||
contains(specs.PreflightsV1Beta2, *tt.wantPreflightSpec),
|
||||
"expected %v to contain %v", specs.PreflightsV1Beta2, *tt.wantPreflightSpec,
|
||||
)
|
||||
}
|
||||
for _, wantUploadResultSpec := range tt.wantUploadResultSpecs {
|
||||
assert.Truef(t,
|
||||
contains(specs.PreflightsV1Beta2, wantUploadResultSpec),
|
||||
"expected %v to contain %v", specs.PreflightsV1Beta2, wantUploadResultSpec,
|
||||
)
|
||||
}
|
||||
if tt.wantHostPreflightSpec != nil {
|
||||
assert.Truef(t,
|
||||
contains(specs.HostPreflightsV1Beta2, *tt.wantHostPreflightSpec),
|
||||
"expected %v to contain %v", testutils.AsJSON(t, specs.HostPreflightsV1Beta2), testutils.AsJSON(t, specs.HostPreflightsV1Beta2),
|
||||
)
|
||||
}
|
||||
|
||||
assert.Equal(t, specs.PreflightSpec, tt.wantPreflightSpec)
|
||||
assert.Equal(t, specs.HostPreflightSpec, tt.wantHostPreflightSpec)
|
||||
assert.Equal(t, specs.UploadResultSpecs, tt.wantUploadResultSpecs)
|
||||
assert.Len(t, specs.SupportBundlesV1Beta2, 0)
|
||||
assert.Len(t, specs.RedactorsV1Beta2, 0)
|
||||
assert.Len(t, specs.AnalyzersV1Beta2, 0)
|
||||
assert.Len(t, specs.CollectorsV1Beta2, 0)
|
||||
assert.Len(t, specs.RemoteCollectorsV1Beta2, 0)
|
||||
assert.Len(t, specs.HostCollectorsV1Beta2, 0)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func contains[T any](list []T, obj T) bool {
|
||||
for _, item := range list {
|
||||
if assert.ObjectsAreEqual(item, obj) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func concatSpecs(target troubleshootv1beta2.Preflight, source troubleshootv1beta2.Preflight) *troubleshootv1beta2.Preflight {
|
||||
newSpec := target.DeepCopy()
|
||||
newSpec.Spec.Collectors = append(newSpec.Spec.Collectors, source.Spec.Collectors...)
|
||||
newSpec.Spec.RemoteCollectors = append(newSpec.Spec.RemoteCollectors, source.Spec.RemoteCollectors...)
|
||||
newSpec.Spec.Analyzers = append(newSpec.Spec.Analyzers, source.Spec.Analyzers...)
|
||||
|
||||
return newSpec
|
||||
}
|
||||
|
||||
// Structured as a separate function so we can use defer appropriately
|
||||
func singleTestPreflightSpecsRead(t *testing.T, tt *PreflightSpecsReadTest, specs *PreflightSpecs) error {
|
||||
func singleTestPreflightSpecsRead(t *testing.T, tt *PreflightSpecsReadTest) (*loader.TroubleshootKinds, error) {
|
||||
var tmpfile *os.File
|
||||
var err error
|
||||
if tt.customStdin == true {
|
||||
@@ -329,13 +400,13 @@ func singleTestPreflightSpecsRead(t *testing.T, tt *PreflightSpecsReadTest, spec
|
||||
os.Stdin = tmpfile
|
||||
}
|
||||
|
||||
err = specs.Read(tt.args)
|
||||
kinds, err := readSpecs(tt.args)
|
||||
|
||||
if tt.customStdin == true {
|
||||
if err = tmpfile.Close(); err != nil {
|
||||
log.Fatal(err)
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
return kinds, err
|
||||
}
|
||||
|
||||
+25
-22
@@ -41,19 +41,26 @@ func RunPreflights(interactive bool, output string, format string, args []string
|
||||
os.Exit(1)
|
||||
}()
|
||||
|
||||
specs := PreflightSpecs{}
|
||||
err := specs.Read(args)
|
||||
specs, err := readSpecs(args)
|
||||
if err != nil {
|
||||
return err
|
||||
return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, err)
|
||||
}
|
||||
|
||||
warning := validatePreflight(specs)
|
||||
|
||||
if warning != nil {
|
||||
fmt.Println(warning.Warning())
|
||||
return nil
|
||||
}
|
||||
|
||||
if viper.GetBool("dry-run") {
|
||||
out, err := specs.ToYaml()
|
||||
if err != nil {
|
||||
return types.NewExitCodeError(constants.EXIT_CODE_CATCH_ALL, errors.Wrap(err, "failed to convert specs to yaml"))
|
||||
}
|
||||
fmt.Printf("%s", out)
|
||||
return nil
|
||||
}
|
||||
|
||||
var collectResults []CollectResult
|
||||
var uploadCollectResults []CollectResult
|
||||
preflightSpecName := ""
|
||||
@@ -74,41 +81,37 @@ func RunPreflights(interactive bool, output string, format string, args []string
|
||||
|
||||
uploadResultsMap := make(map[string][]CollectResult)
|
||||
|
||||
if specs.PreflightSpec != nil {
|
||||
r, err := collectInCluster(ctx, specs.PreflightSpec, progressCh)
|
||||
for _, spec := range specs.PreflightsV1Beta2 {
|
||||
r, err := collectInCluster(ctx, &spec, progressCh)
|
||||
if err != nil {
|
||||
return types.NewExitCodeError(constants.EXIT_CODE_CATCH_ALL, errors.Wrap(err, "failed to collect in cluster"))
|
||||
}
|
||||
collectResults = append(collectResults, *r)
|
||||
preflightSpecName = specs.PreflightSpec.Name
|
||||
}
|
||||
if specs.UploadResultSpecs != nil {
|
||||
for _, spec := range specs.UploadResultSpecs {
|
||||
r, err := collectInCluster(ctx, spec, progressCh)
|
||||
if err != nil {
|
||||
return types.NewExitCodeError(constants.EXIT_CODE_CATCH_ALL, errors.Wrap(err, "failed to collect in cluster"))
|
||||
}
|
||||
if spec.Spec.UploadResultsTo != "" {
|
||||
uploadResultsMap[spec.Spec.UploadResultsTo] = append(uploadResultsMap[spec.Spec.UploadResultsTo], *r)
|
||||
uploadCollectResults = append(collectResults, *r)
|
||||
preflightSpecName = spec.Name
|
||||
} else {
|
||||
collectResults = append(collectResults, *r)
|
||||
}
|
||||
// TODO: This spec name will be overwritten by the next spec. Is this intentional?
|
||||
preflightSpecName = spec.Name
|
||||
}
|
||||
if specs.HostPreflightSpec != nil {
|
||||
if len(specs.HostPreflightSpec.Spec.Collectors) > 0 {
|
||||
r, err := collectHost(ctx, specs.HostPreflightSpec, progressCh)
|
||||
|
||||
for _, spec := range specs.HostPreflightsV1Beta2 {
|
||||
if len(spec.Spec.Collectors) > 0 {
|
||||
r, err := collectHost(ctx, &spec, progressCh)
|
||||
if err != nil {
|
||||
return types.NewExitCodeError(constants.EXIT_CODE_CATCH_ALL, errors.Wrap(err, "failed to collect from host"))
|
||||
}
|
||||
collectResults = append(collectResults, *r)
|
||||
}
|
||||
if len(specs.HostPreflightSpec.Spec.RemoteCollectors) > 0 {
|
||||
r, err := collectRemote(ctx, specs.HostPreflightSpec, progressCh)
|
||||
if len(spec.Spec.RemoteCollectors) > 0 {
|
||||
r, err := collectRemote(ctx, &spec, progressCh)
|
||||
if err != nil {
|
||||
return types.NewExitCodeError(constants.EXIT_CODE_CATCH_ALL, errors.Wrap(err, "failed to collect remotely"))
|
||||
}
|
||||
collectResults = append(collectResults, *r)
|
||||
}
|
||||
preflightSpecName = specs.HostPreflightSpec.Name
|
||||
preflightSpecName = spec.Name
|
||||
}
|
||||
|
||||
if collectResults == nil && uploadCollectResults == nil {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user