diff --git a/.github/workflows/companion.yml b/.github/workflows/companion.yml new file mode 100644 index 0000000..e7169a6 --- /dev/null +++ b/.github/workflows/companion.yml @@ -0,0 +1,47 @@ +--- +name: Build heap-dump-companion + +on: + push: + branches: + - feature/tt/onboarding + paths-ignore: + - 'heap-dump-service/**' + - 'notify-sidecar/**' + +env: + PROJECT_PATH: heap-dump-companion + +jobs: + build-companion: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: '1.22' + + - name: Go Test + run: | + cd ${{ env.PROJECT_PATH }} + go test -v ./... + + - name: Trivy Scan + uses: aquasecurity/trivy-action@0.29.0 + with: + scan-type: 'fs' + path: ${{ env.PROJECT_PATH }} + + - name: Run GoReleaser (snapshot) + uses: goreleaser/goreleaser-action@v4 + with: + workdir: ${{ env.PROJECT_PATH }} + version: latest + args: build --snapshot --clean \ No newline at end of file diff --git a/.github/workflows/container-build.yml b/.github/workflows/container-build.yml new file mode 100644 index 0000000..040410d --- /dev/null +++ b/.github/workflows/container-build.yml @@ -0,0 +1,74 @@ +--- +name: Build, Test, Scan, and Push OCI Images + +on: + push: + branches: + - feature/tt/onboarding + paths-ignore: + - 'heap-dump-companion/**' + +jobs: + build-and-push-images: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + strategy: + matrix: + include: + - image: heap-dump-service + dockerfile: heap-dump-service/Dockerfile + gopath: heap-dump-service + ghcr-image: ghcr.io/${{ github.repository }}/heap-dump-service + - image: notify-sidecar + dockerfile: notify-sidecar/Dockerfile + gopath: notify-sidecar + ghcr-image: ghcr.io/${{ github.repository }}/notify-sidecar + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Log in to GHCR + uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v4 + with: + images: ${{ matrix.ghcr-image }} + + - name: Go Test + run: | + cd ${{ matrix.gopath }} + go test -v ./... + + - name: Build + uses: docker/build-push-action@v6 + with: + context: ${{ matrix.image }} + file: ${{ matrix.dockerfile }} + push: false + tags: ${{ matrix.ghcr-image }}:latest + labels: ${{ steps.meta.outputs.labels }} + + - name: Trivy Scan + uses: aquasecurity/trivy-action@0.29.0 + with: + image-ref: ${{ matrix.ghcr-image }}:latest + format: table + severity: CRITICAL,HIGH + + - name: Push + if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} + uses: docker/build-push-action@v6 + with: + context: ${{ matrix.image }} + file: ${{ matrix.dockerfile }} + push: true + tags: ${{ matrix.ghcr-image }}:latest + labels: ${{ steps.meta.outputs.labels }} \ No newline at end of file diff --git a/.github/workflows/release-companion.yml b/.github/workflows/release-companion.yml new file mode 100644 index 0000000..e7b9330 --- /dev/null +++ b/.github/workflows/release-companion.yml @@ -0,0 +1,43 @@ +--- +name: Release heap-dump-companion + +on: + release: + types: [created] + +env: + PROJECT_PATH: heap-dump-companion + +jobs: + release-companion: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: '1.22' + + - name: Go Test + run: | + cd ${{ env.PROJECT_PATH }} + go test -v ./... + + - name: Trivy Scan + uses: aquasecurity/trivy-action@0.29.0 + with: + scan-type: 'fs' + path: ${{ env.PROJECT_PATH }} + + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v4 + with: + workdir: ${{ env.PROJECT_PATH }} + version: latest + args: release --clean \ No newline at end of file diff --git a/.github/workflows/release-container.yml b/.github/workflows/release-container.yml new file mode 100644 index 0000000..f1dd25d --- /dev/null +++ b/.github/workflows/release-container.yml @@ -0,0 +1,70 @@ +--- +name: Release OCI images + +on: + release: + types: [created] + +jobs: + release-images: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + strategy: + matrix: + include: + - image: heap-dump-service + dockerfile: heap-dump-service/Dockerfile + gopath: heap-dump-service + ghcr-image: ghcr.io/${{ github.repository }}/heap-dump-service + - image: notify-sidecar + dockerfile: notify-sidecar/Dockerfile + gopath: notify-sidecar + ghcr-image: ghcr.io/${{ github.repository }}/notify-sidecar + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Log in to GHCR + uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v4 + with: + images: ${{ matrix.ghcr-image }} + + - name: Go Test + run: | + cd ${{ matrix.gopath }} + go test -v ./... + + - name: Build + uses: docker/build-push-action@v6 + with: + context: ${{ matrix.image }} + file: ${{ matrix.dockerfile }} + push: false + tags: ${{ matrix.ghcr-image }}:${{ github.event.release.name }}" + labels: ${{ steps.meta.outputs.labels }} + + - name: Trivy Scan + uses: aquasecurity/trivy-action@0.29.0 + with: + image-ref: ${{ matrix.ghcr-image }}:${{ github.event.release.name }}" + format: table + severity: CRITICAL,HIGH + + - name: Push + uses: docker/build-push-action@v6 + with: + context: ${{ matrix.image }} + file: ${{ matrix.dockerfile }} + push: true + tags: ${{ matrix.ghcr-image }}:${{ github.event.release.name }}" + labels: ${{ steps.meta.outputs.labels }} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..93b7d2c --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +# Heap Dump Management + +As the filesystem in Kubernetes pods is not directly accessable we developed a sidecar approach where written heap dumps are collected and stored in an encrypted format on a central S3 Bucket per cluster. + +This approach consists of 3 parts: + +* [heap dump service](heap-dump-service/README.md) +* [notify sidecar](notify-sidecar/README.md) +* [heap dump companion](heap-dump-companion/README.md) + +Please read the individual documentation to understand the individual parts. +This architectual overview should help to understand the interactions in between the components + +![](notify-sidecar/docs/Architecture.svg) + +## Maintainers + +This project is maintained by: +* Tobias Trabelsi (tobias.trabelsi+github@dbschenker.com) diff --git a/heap-dump-companion/.gitignore b/heap-dump-companion/.gitignore new file mode 100644 index 0000000..f64001b --- /dev/null +++ b/heap-dump-companion/.gitignore @@ -0,0 +1,3 @@ +bin/* +test +dist \ No newline at end of file diff --git a/heap-dump-companion/.goreleaser.yaml b/heap-dump-companion/.goreleaser.yaml new file mode 100644 index 0000000..36fdd00 --- /dev/null +++ b/heap-dump-companion/.goreleaser.yaml @@ -0,0 +1,42 @@ +version: 2 +project_name: heap-dump-companion +before: + hooks: + - go mod tidy +builds: + - env: + - CGO_ENABLED=0 + mod_timestamp: '{{ .CommitTimestamp }}' + flags: + - -trimpath + ldflags: + - '-s -w' + goos: + - windows + - linux + - darwin + goarch: + - amd64 + - '386' + - arm + - arm64 + ignore: + - goos: darwin + goarch: '386' + dir: cmd/heap-dump-companion +archives: +- format: zip + name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}' +checksum: + name_template: '{{ .ProjectName }}_{{ .Version }}_SHA256SUMS' + algorithm: sha256 +snapshot: + version_template: "snapshot" +changelog: + sort: asc + filters: + exclude: + - '^docs:' + - '^test:' +sboms: + - artifacts: binary diff --git a/heap-dump-companion/Makefile b/heap-dump-companion/Makefile new file mode 100644 index 0000000..d611db4 --- /dev/null +++ b/heap-dump-companion/Makefile @@ -0,0 +1,35 @@ +GOFMT ?= gofmt -s +GOFMT_FILES?=$$(find . -name '*.go'|grep -v .cache) + +build: test + goreleaser build --snapshot --clean + +package: build + goreleaser release --skip-publish --snapshot --clean + +test: fmt-check + go generate ./...; \ + go test ./... -coverprofile=coverage.out; \ + go test ./... -json > report.json; + +fmt-check: + @diff=$$($(GOFMT) -d $(GOFMT_FILES)); \ + if [ -n "$$diff" ]; then \ + echo "Please run 'make fmt' and commit the result:"; \ + echo "$${diff}"; \ + exit 1; \ + fi; + +fmt: + go fmt ./... + +clean: + rm -rf dist; \ + rm coverage.out; \ + rm report.json; + +.PHONY: build + +all: + $(MAKE) build + $(MAKE) package \ No newline at end of file diff --git a/heap-dump-companion/README.md b/heap-dump-companion/README.md new file mode 100644 index 0000000..ad1857f --- /dev/null +++ b/heap-dump-companion/README.md @@ -0,0 +1,49 @@ +# Heap Dump Companion + +Helper binary to seemlesly work with encrypted heap dumps. + +![](docs/Architecture.svg) + +## What it does + +As all heap dumps are AES encrypted and the AES Key itself is encrypted with Hashicorp Vault's transit encryption, we offer a small companion CLI application to decrypt the heap dump and the AES key in one go. + +### MacOS prerequisites + +Maybe OSX is blocking you from execution of downloaded tool.\ +Go to directory with extracted tool and execute in shell: +```bash +xattr -d com.apple.quarantine heap-dump-companion +``` + +The tool should now be executable. + +### Usage of heap-dump-companion + +Make sure that you are signed into Vault and export your vault token via the environment variable `VAULT_TOKEN`. + +``` +Companion implementation intended to work with the general heap dump service. + +This command takes a encrypted heap dump, the encrypted AES Key of the heap dump and decrypts both +using the transit engine of hashicorp Vault. + +Examples: + +heap-dump-companion decrypt --input-file test/test.dump.crypted --output-file test/test.dump --key test/test.key -t some-tenant + +Usage: + heap-dump-companion decrypt [flags] + +Flags: + -h, --help help for decrypt + -i, --input-file string Path to the encrypted heap dump + -k, --key string Path to the encrypted key that should be used for dectyption + -o, --output-file string Desired output file after decryption + -t, --topic string Topic/Tenant owner of the heap dump to be decrypted + -T, --transit-mount-point string Transit engine mount point in vault (default "eaas-heap-dump-service") + +Global Flags: + -c, --config string config file (default is $HOME/.heap-dump-companion.yaml) +``` + diff --git a/heap-dump-companion/cmd/heap-dump-companion/functions/decrypt.go b/heap-dump-companion/cmd/heap-dump-companion/functions/decrypt.go new file mode 100644 index 0000000..48b9e3f --- /dev/null +++ b/heap-dump-companion/cmd/heap-dump-companion/functions/decrypt.go @@ -0,0 +1,65 @@ +package functions + +import ( + "encoding/base64" + "os" + "path/filepath" + + "github.com/dbschenker/heap-dump-management/heap-dump-companion/internal/decrypt" + "github.com/dbschenker/heap-dump-management/heap-dump-companion/internal/vault" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var heapDumpLocation string +var output string +var aesKeyLocation string +var topic string +var transitMountPoint string + +var decryptCmd = &cobra.Command{ + Use: "decrypt", + Short: "Decrypt a provided file with a encrypted key using hashicorp vault", + Long: `Companion implementation intended to work with the general heap dump service. + +This command takes a encrypted heap dump, the encrypted AES Key of the heap dump and decrypts both +using the transit engine of hashicorp Vault. + +Examples: + +heap-dump-companion decrypt --input-file test/test.dump.crypted --output-file test/test.dump --key test/test.key -t some-tenant`, + Run: func(cmd *cobra.Command, args []string) { + client, err := vault.GenerateTransitVaultClient() + cobra.CheckErr(err) + fullAesKeyLocation, err := filepath.Abs(aesKeyLocation) + cobra.CheckErr(err) + encryptedKey, err := os.ReadFile(fullAesKeyLocation) + cobra.CheckErr(err) + plainTextKey, err := vault.TransitDecryptString(client, transitMountPoint, viper.GetString("topic"), string(encryptedKey)) + cobra.CheckErr(err) + decodedKey, err := base64.StdEncoding.DecodeString(plainTextKey) + cobra.CheckErr(err) + fullOutputLocation, err := filepath.Abs(output) + cobra.CheckErr(err) + fullHeapDumpLocation, err := filepath.Abs(heapDumpLocation) + cobra.CheckErr(err) + dir, file := filepath.Split(fullHeapDumpLocation) + err = decrypt.DecryptFile(os.DirFS(dir), decodedKey, file, fullOutputLocation) + cobra.CheckErr(err) + }, +} + +func init() { + rootCmd.AddCommand(decryptCmd) + decryptCmd.PersistentFlags().StringVarP(&heapDumpLocation, "input-file", "i", "", "Path to the encrypted heap dump") + decryptCmd.PersistentFlags().StringVarP(&output, "output-file", "o", "", "Desired output file after decryption") + decryptCmd.PersistentFlags().StringVarP(&aesKeyLocation, "key", "k", "", "Path to the encrypted key that should be used for dectyption") + decryptCmd.PersistentFlags().StringVarP(&topic, "topic", "t", "", "Topic/Tenant owner of the heap dump to be decrypted") + decryptCmd.PersistentFlags().StringVarP(&transitMountPoint, "transit-mount-point", "T", "eaas-heap-dump-service", "Transit engine mount point in vault") + + decryptCmd.MarkFlagRequired("input-file") + decryptCmd.MarkFlagRequired("output-file") + decryptCmd.MarkFlagRequired("key") + + viper.BindPFlag("topic", decryptCmd.PersistentFlags().Lookup("topic")) +} diff --git a/heap-dump-companion/cmd/heap-dump-companion/functions/root.go b/heap-dump-companion/cmd/heap-dump-companion/functions/root.go new file mode 100644 index 0000000..79e86f3 --- /dev/null +++ b/heap-dump-companion/cmd/heap-dump-companion/functions/root.go @@ -0,0 +1,55 @@ +package functions + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var ( + cfgFile string + + rootCmd = &cobra.Command{ + + Use: "heap-dump-companion", + Short: "heap-dump-companion - decrypt heap dumps with vault", + Long: `heap-dump-companion is a super fancy CLI (kidding) to decrypt heap dumps with vault + +you can easily decrypt encrypted heap dumps with the transit engine from hashicorp vault. +Just make sure that you are signed in before usage, else this will always result in a permission denied`, + } +) + +func Execute() { + err := rootCmd.Execute() + if err != nil { + os.Exit(1) + } + viper.WriteConfig() +} + +func init() { + cobra.OnInitialize(initConfig) + rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file (default is $HOME/.heap-dump-companion.yaml)") +} + +func initConfig() { + if cfgFile != "" { + viper.SetConfigFile(cfgFile) + } else { + home, err := os.UserHomeDir() + cobra.CheckErr(err) + + viper.AddConfigPath(home) + viper.SetConfigType("yaml") + viper.SetConfigName(".heap-dump-companion") + } + + viper.AutomaticEnv() + + if err := viper.ReadInConfig(); err == nil { + fmt.Println("Using config file:", viper.ConfigFileUsed()) + } +} diff --git a/heap-dump-companion/cmd/heap-dump-companion/main.go b/heap-dump-companion/cmd/heap-dump-companion/main.go new file mode 100644 index 0000000..1fe7db4 --- /dev/null +++ b/heap-dump-companion/cmd/heap-dump-companion/main.go @@ -0,0 +1,7 @@ +package main + +import "github.com/dbschenker/heap-dump-management/heap-dump-companion/cmd/heap-dump-companion/functions" + +func main() { + functions.Execute() +} diff --git a/heap-dump-companion/docs/Architecture.svg b/heap-dump-companion/docs/Architecture.svg new file mode 100644 index 0000000..2f22c20 --- /dev/null +++ b/heap-dump-companion/docs/Architecture.svg @@ -0,0 +1 @@ +heap dump companionheap dump companioncreate vault transit clientdecrypt aes key with transit client and tenant keydecrypt heap dumpwrite heap dump to disk \ No newline at end of file diff --git a/heap-dump-companion/docs/architecture.plantuml b/heap-dump-companion/docs/architecture.plantuml new file mode 100644 index 0000000..4aa42b4 --- /dev/null +++ b/heap-dump-companion/docs/architecture.plantuml @@ -0,0 +1,11 @@ +@startuml Architecture +title heap dump companion + +start +:create vault transit client; +:decrypt aes key with transit client and tenant key; +:decrypt heap dump; +:write heap dump to disk; +stop + +@enduml \ No newline at end of file diff --git a/heap-dump-companion/go.mod b/heap-dump-companion/go.mod new file mode 100644 index 0000000..21bbdfc --- /dev/null +++ b/heap-dump-companion/go.mod @@ -0,0 +1,98 @@ +module github.com/dbschenker/heap-dump-management/heap-dump-companion + +go 1.22.7 + +toolchain go1.23.3 + +require ( + github.com/docker/go-connections v0.5.0 + github.com/mittwald/vaultgo v0.1.9 + github.com/sirupsen/logrus v1.9.3 + github.com/spf13/cobra v1.8.1 + github.com/spf13/viper v1.19.0 + github.com/testcontainers/testcontainers-go v0.34.0 +) + +//replace github.com/docker/docker => github.com/docker/docker v20.10.3-0.20221013203545-33ab36d6b304+incompatible // 22.06 branch + +require ( + dario.cat/mergo v1.0.1 // indirect + github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 // indirect + github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/docker v27.4.0+incompatible // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fsnotify/fsnotify v1.8.0 // indirect + github.com/go-jose/go-jose/v4 v4.0.4 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-retryablehttp v0.7.7 // indirect + github.com/hashicorp/go-rootcerts v1.0.2 // indirect + github.com/hashicorp/go-secure-stdlib/parseutil v0.1.8 // indirect + github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect + github.com/hashicorp/go-sockaddr v1.0.7 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/hashicorp/vault/api v1.15.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/klauspost/compress v1.17.11 // indirect + github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect + github.com/magiconair/properties v1.8.9 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/sys/sequential v0.6.0 // indirect + github.com/moby/sys/user v0.3.0 // indirect + github.com/moby/sys/userns v0.1.0 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect + github.com/ryanuber/go-glob v1.0.0 // indirect + github.com/sagikazarmark/locafero v0.6.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/shirou/gopsutil/v3 v3.24.5 // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.7.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/stretchr/testify v1.10.0 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/tklauser/go-sysconf v0.3.14 // indirect + github.com/tklauser/numcpus v0.9.0 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect + go.opentelemetry.io/otel v1.33.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 // indirect + go.opentelemetry.io/otel/metric v1.33.0 // indirect + go.opentelemetry.io/otel/sdk v1.33.0 // indirect + go.opentelemetry.io/otel/trace v1.33.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/crypto v0.31.0 // indirect + golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e // indirect + golang.org/x/net v0.32.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect + golang.org/x/time v0.8.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/heap-dump-companion/go.sum b/heap-dump-companion/go.sum new file mode 100644 index 0000000..0475eb5 --- /dev/null +++ b/heap-dump-companion/go.sum @@ -0,0 +1,263 @@ +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v27.4.0+incompatible h1:I9z7sQ5qyzO0BfAb9IMOawRkAGxhYsidKiTMcm0DU+A= +github.com/docker/docker v27.4.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E= +github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw= +github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= +github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= +github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-secure-stdlib/parseutil v0.1.8 h1:iBt4Ew4XEGLfh6/bPk4rSYmuZJGizr6/x/AEizP0CQc= +github.com/hashicorp/go-secure-stdlib/parseutil v0.1.8/go.mod h1:aiJI+PIApBRQG7FZTEBx5GiiX+HbOHilUdNxUZi4eV0= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= +github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw= +github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/vault/api v1.15.0 h1:O24FYQCWwhwKnF7CuSqP30S51rTV7vz1iACXE/pj5DA= +github.com/hashicorp/vault/api v1.15.0/go.mod h1:+5YTO09JGn0u+b6ySD/LLVf8WkJCPLAL2Vkmrn2+CM8= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0= +github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k= +github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM= +github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mittwald/vaultgo v0.1.9 h1:IVWYoxGmI75w8gZM8Y7z9FsNI2/dwd5dJs13VtOo+Vc= +github.com/mittwald/vaultgo v0.1.9/go.mod h1:MuFKjvIXDjRU8cVxAKS/12JcxxzRCWzbdDcPC8sGdQQ= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo= +github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= +github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= +github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk= +github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= +github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= +github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= +github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= +github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/testcontainers/testcontainers-go v0.34.0 h1:5fbgF0vIN5u+nD3IWabQwRybuB4GY8G2HHgCkbMzMHo= +github.com/testcontainers/testcontainers-go v0.34.0/go.mod h1:6P/kMkQe8yqPHfPWNulFGdFHTD8HB2vLq/231xY2iPQ= +github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU= +github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY= +github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPDo= +github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS604NSRyI= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= +go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw= +go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 h1:wpMfgF8E1rkrT1Z6meFh1NDtownE9Ii3n3X2GJYjsaU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0= +go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ= +go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M= +go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM= +go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM= +go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s= +go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck= +go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg= +go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +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.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e h1:4qufH0hlUYs6AO6XmZC3GqfDPGSXHVXUFR6OND+iJX4= +golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= +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/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +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-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= +golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= +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/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-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= +golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +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/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/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y= +google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q= +google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 h1:8ZmaLZE4XWrtU3MyClkYqqtl6Oegr3235h7jxsDyqCY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= +google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0= +google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw= +google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= +google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= diff --git a/heap-dump-companion/internal/decrypt/decrypt.go b/heap-dump-companion/internal/decrypt/decrypt.go new file mode 100644 index 0000000..bd7c8d6 --- /dev/null +++ b/heap-dump-companion/internal/decrypt/decrypt.go @@ -0,0 +1,45 @@ +package decrypt + +import ( + "crypto/aes" + "crypto/cipher" + "errors" + "fmt" + "io/fs" + "os" +) + +func DecryptFile(fileSystem fs.FS, key []byte, encryptedFileLocation string, desiredOutputFileLocation string) error { + // Reading ciphertext file + cipherText, err := fs.ReadFile(fileSystem, encryptedFileLocation) + if err != nil { + return errors.New(fmt.Sprintf("Error reading heap dump %s: %s", encryptedFileLocation, err.Error())) + } + + // Creating block of algorithm + block, err := aes.NewCipher(key) + if err != nil { + return errors.New(fmt.Sprintf("Error initializing ARE Cipher: %s", err.Error())) + } + + // Creating GCM mode + gcm, err := cipher.NewGCM(block) + if err != nil { + return errors.New(fmt.Sprintf("Error in GCM Cipher: %s", err.Error())) + } + + // Deattached nonce and decrypt + nonce := cipherText[:gcm.NonceSize()] + cipherText = cipherText[gcm.NonceSize():] + plainText, err := gcm.Open(nil, nonce, cipherText, nil) + if err != nil { + return errors.New(fmt.Sprintf("Decrypting file %s failed: %s", encryptedFileLocation, err.Error())) + } + + // Writing decryption content + err = os.WriteFile(desiredOutputFileLocation, plainText, 0666) + if err != nil { + return errors.New(fmt.Sprintf("Error writing decrypted heap dump: %s", err.Error())) + } + return nil +} diff --git a/heap-dump-companion/internal/decrypt/decrypt_test.go b/heap-dump-companion/internal/decrypt/decrypt_test.go new file mode 100644 index 0000000..f249e99 --- /dev/null +++ b/heap-dump-companion/internal/decrypt/decrypt_test.go @@ -0,0 +1,93 @@ +package decrypt + +import ( + "os" + "strings" + "testing" + "testing/fstest" +) + +func cleanup(target string) { + os.Remove(target) +} + +func TestDecrypt(t *testing.T) { + fs := fstest.MapFS{ + "test_heap_dump.crypted": {Data: []byte{249, 184, 229, 140, 162, 106, 204, 138, 217, 134, 0, 193, 0, 94, 138, 198, 87, 151, 61, 2, 150, 92, 171, 128, 156, 23, 5, 153, 140, 69, 83, 173, 163, 164, 4, 58, 155, 75, 53, 198}}, + } + test_key := []byte{52, 74, 93, 7, 97, 74, 50, 186, 172, 14, 125, 208, 130, 218, 177, 215, 219, 219, 247, 163, 81, 86, 105, 60, 22, 162, 54, 81, 19, 37, 212, 49} + testTargetFile := "/tmp/test_heap_dump" + want := "asdfasdfasdf" + err := DecryptFile(fs, test_key, "test_heap_dump.crypted", testTargetFile) + + if err != nil { + t.Errorf("Failed to encrypt test file: %v", err) + } + _, err = os.Stat(testTargetFile) + if err != nil { + t.Errorf("Encrypted test file does not exist: %v", err) + } + + clearText, err := os.ReadFile(testTargetFile) + if err != nil { + t.Errorf("Encrypted test file does not exist: %v", err) + } + if string(clearText) != want { + t.Errorf("Unexpected decrypted data: want: %s, got %s", want, string(clearText)) + } + cleanup(want) +} + +func TestFileNotFound(t *testing.T) { + fs := fstest.MapFS{ + "test_heap_dump_with_typo": {Data: []byte{249, 184, 229, 140, 162, 106, 204, 138, 217, 134, 0, 193, 0, 94, 138, 198, 87, 151, 61, 2, 150, 92, 171, 128, 156, 23, 5, 153, 140, 69, 83, 173, 163, 164, 4, 58, 155, 75, 53, 198}}, + } + test_key := []byte{52, 74, 93, 7, 97, 74, 50, 186, 172, 14, 125, 208, 130, 218, 177, 215, 219, 219, 247, 163, 81, 86, 105, 60, 22, 162, 54, 81, 19, 37, 212, 49} + testTargetFile := "/tmp/test_heap_dump" + want := "Error reading heap dump" + err := DecryptFile(fs, test_key, "test_heap_dump.crypted", testTargetFile) + + if err == nil { + t.Errorf("Failed to encrypt test file: %v", err) + } + + if !strings.Contains(err.Error(), want) { + t.Errorf("Got %s want: %s", err.Error(), want) + } +} + +func TestBadAESKey(t *testing.T) { + fs := fstest.MapFS{ + "test_heap_dump.crypted": {Data: []byte{249, 184, 229, 140, 162, 106, 204, 138, 217, 134, 0, 193, 0, 94, 138, 198, 87, 151, 61, 2, 150, 92, 171, 128, 156, 23, 5, 153, 140, 69, 83, 173, 163, 164, 4, 58, 155, 75, 53, 198}}, + } + test_key := []byte{52, 74, 93, 7, 97, 74, 50, 186, 172, 14, 125, 208, 130, 218, 177, 215, 219, 219, 247, 163, 81, 86, 105, 60} + testTargetFile := "/tmp/test_heap_dump" + want := "message authentication failed" + err := DecryptFile(fs, test_key, "test_heap_dump.crypted", testTargetFile) + + if err == nil { + t.Errorf("Failed to encrypt test file: %v", err) + } + + if !strings.Contains(err.Error(), want) { + t.Errorf("Got %s want: %s", err.Error(), want) + } +} + +func TestBadOutputFile(t *testing.T) { + fs := fstest.MapFS{ + "test_heap_dump.crypted": {Data: []byte{249, 184, 229, 140, 162, 106, 204, 138, 217, 134, 0, 193, 0, 94, 138, 198, 87, 151, 61, 2, 150, 92, 171, 128, 156, 23, 5, 153, 140, 69, 83, 173, 163, 164, 4, 58, 155, 75, 53, 198}}, + } + test_key := []byte{52, 74, 93, 7, 97, 74, 50, 186, 172, 14, 125, 208, 130, 218, 177, 215, 219, 219, 247, 163, 81, 86, 105, 60, 22, 162, 54, 81, 19, 37, 212, 49} + testTargetFile := "/asdf/test_heap_dump" + want := "Error writing decrypted heap dump" + err := DecryptFile(fs, test_key, "test_heap_dump.crypted", testTargetFile) + + if err == nil { + t.Errorf("Failed to encrypt test file: %v", err) + } + + if !strings.Contains(err.Error(), want) { + t.Errorf("Got %s want: %s", err.Error(), want) + } +} diff --git a/heap-dump-companion/internal/logging/logging.go b/heap-dump-companion/internal/logging/logging.go new file mode 100644 index 0000000..a67b18f --- /dev/null +++ b/heap-dump-companion/internal/logging/logging.go @@ -0,0 +1,13 @@ +package logging + +import ( + "os" + + log "github.com/sirupsen/logrus" +) + +func SetupLogging() { + log.SetFormatter(&log.JSONFormatter{}) + log.SetOutput(os.Stdout) + log.SetLevel(log.InfoLevel) +} diff --git a/heap-dump-companion/internal/logging/logging_test.go b/heap-dump-companion/internal/logging/logging_test.go new file mode 100644 index 0000000..de46d9d --- /dev/null +++ b/heap-dump-companion/internal/logging/logging_test.go @@ -0,0 +1,22 @@ +package logging + +import ( + "os" + "reflect" + "testing" + + log "github.com/sirupsen/logrus" +) + +func TestSetupLogging(t *testing.T) { + SetupLogging() + if log.GetLevel() != log.InfoLevel { + t.Errorf("got %+v, want %+v", log.GetLevel(), log.InfoLevel) + } + if !reflect.DeepEqual(log.StandardLogger().Formatter, &log.JSONFormatter{}) { + t.Errorf("got %+v, want %+v", log.StandardLogger().Formatter, &log.JSONFormatter{}) + } + if !reflect.DeepEqual(log.StandardLogger().Out, os.Stdout) { + t.Errorf("got %+v, want %+v", log.StandardLogger().Out, os.Stdout) + } +} diff --git a/heap-dump-companion/internal/vault/vault-helper.go b/heap-dump-companion/internal/vault/vault-helper.go new file mode 100644 index 0000000..fff2e07 --- /dev/null +++ b/heap-dump-companion/internal/vault/vault-helper.go @@ -0,0 +1,58 @@ +package vault + +import ( + "errors" + "fmt" + "os" + + vaultTransit "github.com/mittwald/vaultgo" + log "github.com/sirupsen/logrus" +) + +func GenerateTransitVaultClient() (*vaultTransit.Client, error) { + + vaultURL, found := os.LookupEnv("VAULT_ADDR") + if !found { + log.WithFields(log.Fields{ + "caller": "GenerateTransitVaultClient", + }).Error(fmt.Sprintf("Could not find valid vault URL on env: %s", "VAULT_ADDR")) + return nil, errors.New(fmt.Sprintf("Could not find valid vault URL on env: %s", "VAULT_ADDR")) + } + + vaultToken, found := os.LookupEnv("VAULT_TOKEN") + if !found { + log.WithFields(log.Fields{ + "caller": "GenerateTransitVaultClient", + }).Error(fmt.Sprintf("Could not find valid vault token on env: %s", "VAULT_TOKEN")) + return nil, errors.New(fmt.Sprintf("Could not find valid vault token on env: %s", "VAULT_TOKEN")) + } + + c, err := vaultTransit.NewClient( + vaultURL, + vaultTransit.WithCaPath(""), + vaultTransit.WithAuthToken(vaultToken), + ) + if err != nil { + log.WithFields(log.Fields{ + "caller": "GenerateTransitVaultClient", + }).Error(fmt.Sprintf("Error creating Transit Vault Client: %s", err.Error())) + return nil, err + } + return c, nil +} + +func TransitDecryptString(client *vaultTransit.Client, mountPoint string, topicKey string, cypherText string) (string, error) { + + transit := client.TransitWithMountPoint(mountPoint) + plainTestResponse, err := transit.Decrypt(topicKey, &vaultTransit.TransitDecryptOptions{ + Ciphertext: cypherText, + }) + if err != nil { + log.WithFields(log.Fields{ + "caller": "TransitDecryptString", + }).Error(fmt.Sprintf("Could not decrypt AES Key: %s", err.Error())) + return "", errors.New(fmt.Sprintf("Could not decrypt AES Key: %s", err.Error())) + + } + return plainTestResponse.Data.Plaintext, nil +} diff --git a/heap-dump-companion/internal/vault/vault-helper_test.go b/heap-dump-companion/internal/vault/vault-helper_test.go new file mode 100644 index 0000000..e0058a1 --- /dev/null +++ b/heap-dump-companion/internal/vault/vault-helper_test.go @@ -0,0 +1,182 @@ +package vault + +import ( + "context" + "fmt" + "os" + "testing" + + "github.com/docker/go-connections/nat" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" + + vaultTransit "github.com/mittwald/vaultgo" +) + +var Token = "test" + +var TestTransitKey = "eyJwb2xpY3kiOnsibmFtZSI6InRlc3Qta2V5Iiwia2V5cyI6eyIxIjp7ImtleSI6InEyRU9ra0JhaTFpYkNtSE1RR3RocGQ4TEJodWs1Z3ZWZVRoRzY0ZW84YkE9IiwiaG1hY19rZXkiOiJrQk9maTlkNjl2L1QvbjYvMGg1WU9hUlRUVUZtejE4Q3ZCMFg0UlJiQ2hJPSIsInRpbWUiOiIyMDIzLTAyLTEzVDE1OjQyOjQ2Ljk3NDU4OTM2MVoiLCJlY194IjpudWxsLCJlY195IjpudWxsLCJlY19kIjpudWxsLCJyc2Ffa2V5IjpudWxsLCJwdWJsaWNfa2V5IjoiIiwiY29udmVyZ2VudF92ZXJzaW9uIjowLCJjcmVhdGlvbl90aW1lIjoxNjc2MzAyOTY2fX0sImRlcml2ZWQiOmZhbHNlLCJrZGYiOjAsImNvbnZlcmdlbnRfZW5jcnlwdGlvbiI6ZmFsc2UsImV4cG9ydGFibGUiOnRydWUsIm1pbl9kZWNyeXB0aW9uX3ZlcnNpb24iOjEsIm1pbl9lbmNyeXB0aW9uX3ZlcnNpb24iOjAsImxhdGVzdF92ZXJzaW9uIjoxLCJhcmNoaXZlX3ZlcnNpb24iOjEsImFyY2hpdmVfbWluX3ZlcnNpb24iOjAsIm1pbl9hdmFpbGFibGVfdmVyc2lvbiI6MCwiZGVsZXRpb25fYWxsb3dlZCI6ZmFsc2UsImNvbnZlcmdlbnRfdmVyc2lvbiI6MCwidHlwZSI6MCwiYmFja3VwX2luZm8iOnsidGltZSI6IjIwMjMtMDItMTNUMTU6NTA6MjcuNzYwODE0ODcyWiIsInZlcnNpb24iOjF9LCJyZXN0b3JlX2luZm8iOm51bGwsImFsbG93X3BsYWludGV4dF9iYWNrdXAiOnRydWUsInZlcnNpb25fdGVtcGxhdGUiOiIiLCJzdG9yYWdlX3ByZWZpeCI6IiIsImF1dG9fcm90YXRlX3BlcmlvZCI6MCwiSW1wb3J0ZWQiOmZhbHNlLCJBbGxvd0ltcG9ydGVkS2V5Um90YXRpb24iOmZhbHNlfSwiYXJjaGl2ZWRfa2V5cyI6eyJrZXlzIjpbeyJrZXkiOm51bGwsImhtYWNfa2V5IjpudWxsLCJ0aW1lIjoiMDAwMS0wMS0wMVQwMDowMDowMFoiLCJlY194IjpudWxsLCJlY195IjpudWxsLCJlY19kIjpudWxsLCJyc2Ffa2V5IjpudWxsLCJwdWJsaWNfa2V5IjoiIiwiY29udmVyZ2VudF92ZXJzaW9uIjowLCJjcmVhdGlvbl90aW1lIjowfSx7ImtleSI6InEyRU9ra0JhaTFpYkNtSE1RR3RocGQ4TEJodWs1Z3ZWZVRoRzY0ZW84YkE9IiwiaG1hY19rZXkiOiJrQk9maTlkNjl2L1QvbjYvMGg1WU9hUlRUVUZtejE4Q3ZCMFg0UlJiQ2hJPSIsInRpbWUiOiIyMDIzLTAyLTEzVDE1OjQyOjQ2Ljk3NDU4OTM2MVoiLCJlY194IjpudWxsLCJlY195IjpudWxsLCJlY19kIjpudWxsLCJyc2Ffa2V5IjpudWxsLCJwdWJsaWNfa2V5IjoiIiwiY29udmVyZ2VudF92ZXJzaW9uIjowLCJjcmVhdGlvbl90aW1lIjoxNjc2MzAyOTY2fV19fQo=" + +type VaultContainer struct { + container testcontainers.Container + mappedPort nat.Port + hostIP string + token string +} + +var Vault *VaultContainer + +func (v *VaultContainer) URI() string { + return fmt.Sprintf("http://%s:%s/", v.HostIP(), v.Port()) +} + +func (v *VaultContainer) Port() string { + return v.mappedPort.Port() +} + +func (v *VaultContainer) HostIP() string { + return v.hostIP +} + +func (v *VaultContainer) Token() string { + return v.token +} + +func (v *VaultContainer) Terminate(ctx context.Context) error { + return v.container.Terminate(ctx) +} + +func InitVaultContainer(ctx context.Context, version string) (*VaultContainer, error) { + port := nat.Port("8200/tcp") + + req := testcontainers.GenericContainerRequest{ + ProviderType: testcontainers.ProviderDocker, + ContainerRequest: testcontainers.ContainerRequest{ + Image: "vault:" + version, + ExposedPorts: []string{string(port)}, + WaitingFor: wait.ForListeningPort(port), + SkipReaper: true, + Env: map[string]string{ + "VAULT_ADDR": fmt.Sprintf("http://0.0.0.0:%s", port.Port()), + "VAULT_DEV_ROOT_TOKEN_ID": Token, + "VAULT_TOKEN": Token, + "VAULT_LOG_LEVEL": "trace", + }, + Cmd: []string{ + "server", + "-dev", + }, + Privileged: true, + }, + } + + v, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: req.ContainerRequest, + Started: true, + }) + if err != nil { + return nil, err + } + + vc := &VaultContainer{ + container: v, + mappedPort: "", + hostIP: "", + token: Token, + } + + vc.hostIP, err = v.Host(ctx) + if err != nil { + return nil, err + } + + vc.mappedPort, err = v.MappedPort(ctx, port) + if err != nil { + return nil, err + } + + _, _, err = vc.container.Exec(ctx, []string{ + "vault", + "secrets", + "enable", + "transit", + }) + if err != nil { + return nil, err + } + + _, _, err = vc.container.Exec(ctx, []string{ + "vault", + "write", + "/transit/restore/test-topic", + fmt.Sprintf("backup=%s", TestTransitKey), + }) + if err != nil { + return nil, err + } + + return vc, nil +} + +func TestMain(m *testing.M) { + var err error + Vault, err = InitVaultContainer(context.Background(), "1.11.4") + if err != nil { + fmt.Printf("Could not start test container for vault: %s", err.Error()) + } + m.Run() +} + +func TestGenerateTransitVaultClient(t *testing.T) { + os.Setenv("VAULT_ADDR", Vault.URI()) + defer os.Unsetenv("VAULT_ADDR") + os.Setenv("VAULT_TOKEN", Vault.Token()) + defer os.Unsetenv("VAULT_TOKEN") + client, err := GenerateTransitVaultClient() + if err != nil { + t.Errorf("Error creating test client: %s", err.Error()) + } + + if client.Client.Address() != Vault.URI() { + t.Errorf("Generated Client missconfigured") + } + + if client.Client.Token() != Vault.Token() { + t.Errorf("Generated Client missconfigured") + } +} + +func TestGenerateTransitVaultClientNoEnv(t *testing.T) { + _, err := GenerateTransitVaultClient() + if err == nil { + t.Errorf("No VAULT_ADDR or VAULT_TOKEN should generate an error!") + } + os.Setenv("VAULT_ADDR", Vault.URI()) + defer os.Unsetenv("VAULT_ADDR") + _, err = GenerateTransitVaultClient() + if err == nil { + t.Errorf("No VAULT_TOKEN should generate an error!") + } +} + +func TestVaultDecryptString(t *testing.T) { + os.Setenv("VAULT_ADDR", Vault.URI()) + defer os.Unsetenv("VAULT_ADDR") + os.Setenv("VAULT_TOKEN", Vault.Token()) + defer os.Unsetenv("VAULT_TOKEN") + + testClient, err := vaultTransit.NewClient(Vault.URI(), vaultTransit.WithCaPath(""), vaultTransit.WithAuthToken(Vault.token)) + if err != nil { + t.Errorf("Error creating test client: %s", err.Error()) + } + + ret, err := TransitDecryptString(testClient, "transit", "test-topic", "vault:v1:QPRzx8HS54xZB2v/7KpBnIojMOulGuudYz12Z2x08rg=") + + if err != nil { + t.Errorf("Error decrypting string: %s", err.Error()) + } + + if ret != "test" { + t.Errorf("Decryption failed! want %v, got %v", "test", ret) + } +} diff --git a/heap-dump-companion/sonar-project.properties b/heap-dump-companion/sonar-project.properties new file mode 100644 index 0000000..28b5816 --- /dev/null +++ b/heap-dump-companion/sonar-project.properties @@ -0,0 +1,11 @@ +sonar.projectKey=com.dbschenker.gilds.devops.heap-dump-management_heap_dump_companion +sonar.projectName=Heap Dump Companion App + +sonar.sources=. +sonar.exclusions=**/*_test.go + +sonar.tests=. +sonar.test.inclusions=**/*_test.go + +sonar.go.coverage.reportPaths=coverage.out +sonar.go.tests.reportPaths=report.json \ No newline at end of file diff --git a/heap-dump-service/.DS_Store b/heap-dump-service/.DS_Store new file mode 100644 index 0000000..20973ee Binary files /dev/null and b/heap-dump-service/.DS_Store differ diff --git a/heap-dump-service/.dockerignore b/heap-dump-service/.dockerignore new file mode 100644 index 0000000..dcbd136 --- /dev/null +++ b/heap-dump-service/.dockerignore @@ -0,0 +1,6 @@ +.venv/ +.vscode/ +test/ +*.sh +.git/ +.cache/ diff --git a/heap-dump-service/.gitignore b/heap-dump-service/.gitignore new file mode 100644 index 0000000..36f971e --- /dev/null +++ b/heap-dump-service/.gitignore @@ -0,0 +1 @@ +bin/* diff --git a/heap-dump-service/.vscode/launch.json b/heap-dump-service/.vscode/launch.json new file mode 100644 index 0000000..c71de4d --- /dev/null +++ b/heap-dump-service/.vscode/launch.json @@ -0,0 +1,19 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch app", + "type": "go", + "request": "launch", + "mode": "auto", + "program":"${workspaceFolder}/cmd/app/main.go", + "env": { + "APP_CONFIG_FILE": "${workspaceFolder}/config/local-config.json", + "GIN_MODE": "debug" + }, + } + ] +} \ No newline at end of file diff --git a/heap-dump-service/Dockerfile b/heap-dump-service/Dockerfile new file mode 100644 index 0000000..a36ccff --- /dev/null +++ b/heap-dump-service/Dockerfile @@ -0,0 +1,11 @@ +FROM golang:1.23 AS build + +WORKDIR /go/src/app +COPY . . + +RUN go mod download +RUN CGO_ENABLED=0 go build -o /go/bin/app cmd/app/main.go + +FROM gcr.io/distroless/static-debian12 +COPY --from=build /go/bin/app / +CMD ["/app"] \ No newline at end of file diff --git a/heap-dump-service/README.md b/heap-dump-service/README.md new file mode 100644 index 0000000..5d2c7dd --- /dev/null +++ b/heap-dump-service/README.md @@ -0,0 +1,17 @@ +# Heap Dump Service + +This is the central heap dump service. A microservice that handles the encryption data and AWS authentication for a central s3 bucket to store encrypted heap dumps. + +![](docs/Architecture.svg) + +## What it does + +As shown in the Architecture the central heap dump service is responsible to handle the Hasicorp Vault and AWS permissions, as well as generating the cryptographic material needed in order to encrypt heap dumps from different tenants using the transit engine from vault. +It also ensures that only valid service accounts request these information as the authentication is checked against the kubernetes API. + +![](docs/diagram.svg) + +### Config and Setup + +see [config.md](docs/config.md) + diff --git a/heap-dump-service/chart/heap-dump-service/.helmignore b/heap-dump-service/chart/heap-dump-service/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/heap-dump-service/chart/heap-dump-service/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/heap-dump-service/chart/heap-dump-service/Chart.yaml b/heap-dump-service/chart/heap-dump-service/Chart.yaml new file mode 100644 index 0000000..c01be56 --- /dev/null +++ b/heap-dump-service/chart/heap-dump-service/Chart.yaml @@ -0,0 +1,8 @@ +apiVersion: v2 +name: heap-dump-service +description: A Helm chart to deploy the central heap dump service +type: application + +version: 1.0.0 + +appVersion: "v1.2.0" diff --git a/heap-dump-service/chart/heap-dump-service/templates/NOTES.txt b/heap-dump-service/chart/heap-dump-service/templates/NOTES.txt new file mode 100644 index 0000000..82b662f --- /dev/null +++ b/heap-dump-service/chart/heap-dump-service/templates/NOTES.txt @@ -0,0 +1,16 @@ +1. Get the application URL by running these commands: +{{- if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "heap-dump-service.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch its status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "heap-dump-service.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "heap-dump-service.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "heap-dump-service.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/heap-dump-service/chart/heap-dump-service/templates/_helpers.tpl b/heap-dump-service/chart/heap-dump-service/templates/_helpers.tpl new file mode 100644 index 0000000..93536cf --- /dev/null +++ b/heap-dump-service/chart/heap-dump-service/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "heap-dump-service.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "heap-dump-service.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "heap-dump-service.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "heap-dump-service.labels" -}} +helm.sh/chart: {{ include "heap-dump-service.chart" . }} +{{ include "heap-dump-service.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "heap-dump-service.selectorLabels" -}} +app.kubernetes.io/name: {{ include "heap-dump-service.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "heap-dump-service.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "heap-dump-service.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/heap-dump-service/chart/heap-dump-service/templates/clusterrole.yaml b/heap-dump-service/chart/heap-dump-service/templates/clusterrole.yaml new file mode 100644 index 0000000..180d903 --- /dev/null +++ b/heap-dump-service/chart/heap-dump-service/templates/clusterrole.yaml @@ -0,0 +1,8 @@ +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: {{ include "heap-dump-service.name" . }}-role +rules: +- apiGroups: ["authentication.k8s.io"] + resources: ["tokenreviews", "subjectaccessreviews"] + verbs: ["create"] diff --git a/heap-dump-service/chart/heap-dump-service/templates/configmap.yaml b/heap-dump-service/chart/heap-dump-service/templates/configmap.yaml new file mode 100644 index 0000000..90eb9e1 --- /dev/null +++ b/heap-dump-service/chart/heap-dump-service/templates/configmap.yaml @@ -0,0 +1,28 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "heap-dump-service.name" . }}-role-cm + labels: + app.kubernetes.io/name: {{ include "heap-dump-service.fullname" . }} + app.kubernetes.io/instance: {{ .Release.Name }} +data: + config.json: |- + { + "app": { + "port": {{ .Values.service.port }}, + "bucket": {{ .Values.heapDumpConfig.bucketName | quote }} + }, + "vault": { + "vaultTransitMount": {{ .Values.heapDumpConfig.vaultMount | quote }}, + "vaultRole": {{ .Values.heapDumpConfig.vaultRole | quote }}, + "vaultAuthMountPath": {{ .Values.heapDumpConfig.vaultAuthMountPath | quote }} + }, + "serviceAccount": { + "jwtokenMountPoint": {{ .Values.heapDumpConfig.jwtokenMountPoint | quote }} + }, + "metrics": { + "port": {{ .Values.heapDumpConfig.prometheus.port }}, + "path": {{ .Values.heapDumpConfig.prometheus.path | quote}} + } + } diff --git a/heap-dump-service/chart/heap-dump-service/templates/deployment.yaml b/heap-dump-service/chart/heap-dump-service/templates/deployment.yaml new file mode 100644 index 0000000..67d6bbb --- /dev/null +++ b/heap-dump-service/chart/heap-dump-service/templates/deployment.yaml @@ -0,0 +1,66 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "heap-dump-service.fullname" . }} + labels: + {{- include "heap-dump-service.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + {{- include "heap-dump-service.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "heap-dump-service.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "heap-dump-service.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + livenessProbe: + {{- toYaml .Values.livenessProbe | nindent 12 }} + readinessProbe: + {{- toYaml .Values.readinessProbe | nindent 12 }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.volumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.volumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/heap-dump-service/chart/heap-dump-service/templates/rolebinding.yaml b/heap-dump-service/chart/heap-dump-service/templates/rolebinding.yaml new file mode 100644 index 0000000..8a96ecd --- /dev/null +++ b/heap-dump-service/chart/heap-dump-service/templates/rolebinding.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "heap-dump-service.name" . }}-binding +subjects: +- kind: ServiceAccount + name: {{ include "heap-dump-service.name" . }} + namespace: {{ .Release.Namespace }} +roleRef: + kind: ClusterRole + name: {{ include "heap-dump-service.name" . }}-role + apiGroup: rbac.authorization.k8s.io \ No newline at end of file diff --git a/heap-dump-service/chart/heap-dump-service/templates/service.yaml b/heap-dump-service/chart/heap-dump-service/templates/service.yaml new file mode 100644 index 0000000..03db13d --- /dev/null +++ b/heap-dump-service/chart/heap-dump-service/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "heap-dump-service.fullname" . }} + labels: + {{- include "heap-dump-service.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "heap-dump-service.selectorLabels" . | nindent 4 }} diff --git a/heap-dump-service/chart/heap-dump-service/templates/serviceaccount.yaml b/heap-dump-service/chart/heap-dump-service/templates/serviceaccount.yaml new file mode 100644 index 0000000..c847e44 --- /dev/null +++ b/heap-dump-service/chart/heap-dump-service/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "heap-dump-service.serviceAccountName" . }} + labels: + {{- include "heap-dump-service.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/heap-dump-service/chart/heap-dump-service/templates/tests/test-connection.yaml b/heap-dump-service/chart/heap-dump-service/templates/tests/test-connection.yaml new file mode 100644 index 0000000..b7b504e --- /dev/null +++ b/heap-dump-service/chart/heap-dump-service/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "heap-dump-service.fullname" . }}-test-connection" + labels: + {{- include "heap-dump-service.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "heap-dump-service.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/heap-dump-service/chart/heap-dump-service/values.yaml b/heap-dump-service/chart/heap-dump-service/values.yaml new file mode 100644 index 0000000..8bffe93 --- /dev/null +++ b/heap-dump-service/chart/heap-dump-service/values.yaml @@ -0,0 +1,94 @@ +# Default values for heap-dump-service. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: ghcr.io/dbschenker/tbd + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Automatically mount a ServiceAccount's API credentials? + automount: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} +podLabels: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 8080 + +heapDumpConfig: + bucketName: "" + vaultMount: "" + vaultRole: "" + vaultAuthMountPath: "" + jwtokenMountPoint: "" + prometheus: + port: 8081 + path: /metrics + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +livenessProbe: + httpGet: + path: / + port: http +readinessProbe: + httpGet: + path: / + port: http + +# Additional volumes on the output Deployment definition. +volumes: [] +# - name: foo +# secret: +# secretName: mysecret +# optional: false + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: [] +# - name: foo +# mountPath: "/etc/foo" +# readOnly: true + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/heap-dump-service/cmd/app/main.go b/heap-dump-service/cmd/app/main.go new file mode 100644 index 0000000..2382f62 --- /dev/null +++ b/heap-dump-service/cmd/app/main.go @@ -0,0 +1,36 @@ +package main + +import ( + "fmt" + + "github.com/dbschenker/heap-dump-management/heap-dump-service/internal/config" + "github.com/dbschenker/heap-dump-management/heap-dump-service/internal/logging" + "github.com/dbschenker/heap-dump-management/heap-dump-service/internal/metrics" + restapi "github.com/dbschenker/heap-dump-management/heap-dump-service/internal/rest-api" + log "github.com/sirupsen/logrus" +) + +func main() { + logging.SetupLogging() + + appConfig, err := config.LoadConfigFromEnvironment("APP_CONFIG_FILE") + + if err != nil { + log.WithFields(log.Fields{ + "caller": "LoadConfigFromEnvironment", + }).Fatalf(fmt.Sprintf("Failed to read Config File: %s", err.Error())) + } + + log.Printf("Configuration loaded. Starting event handler") + + go func() { + for { + log.WithFields(log.Fields{ + "caller": "main", + }).Info("Serving Metrics") + metrics.StartMetricServer(appConfig.Metrics.Port, appConfig.Metrics.Path) + } + }() + + restapi.Serve(&appConfig) +} diff --git a/heap-dump-service/config/docker-config.json b/heap-dump-service/config/docker-config.json new file mode 100644 index 0000000..d73072d --- /dev/null +++ b/heap-dump-service/config/docker-config.json @@ -0,0 +1,18 @@ +{ + "app": { + "port": 8080, + "bucket": "" + }, + "vault": { + "vaultTransitMount": "eaas-heap-dump-service", + "vaultRole": "heap-dump-service", + "vaultAuthMountPath": "kubernetes-toolbox-ref-np-kubernetes" + }, + "serviceAccount": { + "jwtokenMountPoint": "/var/run/secrets/kubernetes.io/serviceaccount/token" + }, + "metrics": { + "port": 8081, + "path": "/metrics" + } +} \ No newline at end of file diff --git a/heap-dump-service/config/local-config.json b/heap-dump-service/config/local-config.json new file mode 100644 index 0000000..7434bb1 --- /dev/null +++ b/heap-dump-service/config/local-config.json @@ -0,0 +1,18 @@ +{ + "app": { + "port": 8080, + "bucket": "gdis-ref-np-s3-virusscan-test" + }, + "vault": { + "vaultTransitMount": "eaas-heap-dump-service", + "vaultRole": "heap-dump-service", + "vaultAuthMountPath": "kubernetes-toolbox-ref-np-kubernetes" + }, + "serviceAccount": { + "jwtokenMountPoint": "/var/run/secrets/kubernetes.io/serviceaccount/token" + }, + "metrics": { + "port": 8081, + "path": "/metrics" + } +} \ No newline at end of file diff --git a/heap-dump-service/config/test/bad-config.json b/heap-dump-service/config/test/bad-config.json new file mode 100644 index 0000000..33c0e97 --- /dev/null +++ b/heap-dump-service/config/test/bad-config.json @@ -0,0 +1,5 @@ +{ + "invalid": { + "data": "here" + } +} \ No newline at end of file diff --git a/heap-dump-service/config/test/test-config.json b/heap-dump-service/config/test/test-config.json new file mode 100644 index 0000000..f5b8747 --- /dev/null +++ b/heap-dump-service/config/test/test-config.json @@ -0,0 +1,18 @@ +{ + "app": { + "port": 8080, + "bucket": "test-bucket" + }, + "vault": { + "vaultTransitMount": "eaas-heap-dump-service", + "vaultRole": "heap-dump-service", + "vaultAuthMountPath": "kubernetes-toolbox-ref-np-kubernetes" + }, + "serviceAccount": { + "jwtokenMountPoint": "/var/run/secrets/kubernetes.io/serviceaccount/token" + }, + "metrics": { + "port": 8081, + "path": "/metrics" + } +} \ No newline at end of file diff --git a/heap-dump-service/docs/Architecture.svg b/heap-dump-service/docs/Architecture.svg new file mode 100644 index 0000000..7670ba7 --- /dev/null +++ b/heap-dump-service/docs/Architecture.svg @@ -0,0 +1 @@ +ApplicationApplicationNotifySidecarNotifySidecarHeapDumpServiceHeapDumpServiceVaultVaultAWSAWSwrites heap dump in shared volumeRequest Upload URLEncrypt AES Key with tenant keyGenerate PreSigned upload URL to S3Returns Key, Encrypted Key and URLEncrypts Heap DumpUploads encrypted heap dump and encrypted AES Key to S3Terminates \ No newline at end of file diff --git a/heap-dump-service/docs/README.md b/heap-dump-service/docs/README.md new file mode 100644 index 0000000..a834f18 --- /dev/null +++ b/heap-dump-service/docs/README.md @@ -0,0 +1,5 @@ +# Example + +## backend + +This is example backend diff --git a/heap-dump-service/docs/architecture.plantuml b/heap-dump-service/docs/architecture.plantuml new file mode 100644 index 0000000..e9728fe --- /dev/null +++ b/heap-dump-service/docs/architecture.plantuml @@ -0,0 +1,12 @@ +@startuml Architecture + +Application -> NotifySidecar: writes heap dump in shared volume +NotifySidecar -> HeapDumpService: Request Upload URL +HeapDumpService -> Vault: Encrypt AES Key with tenant key +HeapDumpService -> AWS: Generate PreSigned upload URL to S3 +HeapDumpService -> NotifySidecar: Returns Key, Encrypted Key and URL +NotifySidecar -> NotifySidecar: Encrypts Heap Dump +NotifySidecar -> AWS: Uploads encrypted heap dump and encrypted AES Key to S3 +Application -> Application: Terminates + +@enduml \ No newline at end of file diff --git a/heap-dump-service/docs/config.md b/heap-dump-service/docs/config.md new file mode 100644 index 0000000..4244a9b --- /dev/null +++ b/heap-dump-service/docs/config.md @@ -0,0 +1,135 @@ +# Setup + +In order to deploy the Heap Dump Service Hasicorp Vault, AWS IAM and kubernetes RBAC configuration is needed. + +## Vault + +```hcl +# Enable Transit Engine in Vault +resource "vault_mount" "heap_dump_encryption_mount" { + path = "eaas-heap-dump-service" + type = "transit" + description = "Transit engine for encrypting heap dump AES Keys" +} + +# Create Encryption key in transit engine for each tenant +resource "vault_transit_secret_backend_key" "heap_dump_service_backend" { + backend = vault_mount.heap_dump_encryption_mount.path + name = var.tenant + type = "aes256-gcm96" + + exportable = false + allow_plaintext_backup = false +} + +# For Service +resource "vault_policy" "heap_dump_service" { + name = format("topic-%s-heap-dump-service", var.tenant) + policy = < "Get Request Presigned URL and encryption key" + +if "ServiceAccount Token is valid" then + + -->[true] "Create Vault transit client" + --> "Create AWS s3 Client" + --> "Generate AES Key" + --> "Encrypt AES Key with transit engine" + --> "Generate AWS Presigned URL" + --> "Return Presigned URL, AES key and encrypted AES key" +else + -->[false] "Return 401" +endif +@enduml \ No newline at end of file diff --git a/heap-dump-service/docs/diagram.svg b/heap-dump-service/docs/diagram.svg new file mode 100644 index 0000000..698c855 --- /dev/null +++ b/heap-dump-service/docs/diagram.svg @@ -0,0 +1 @@ +Get Request Presigned URL and encryption keyCreate Vault transit clientCreate AWS s3 ClientGenerate AES KeyEncrypt AES Key with transit engineGenerate AWS Presigned URLReturn Presigned URL, AES key and encrypted AES keyReturn 401ServiceAccount Token is validtruefalse \ No newline at end of file diff --git a/heap-dump-service/docs/docs.go b/heap-dump-service/docs/docs.go new file mode 100644 index 0000000..2bf127a --- /dev/null +++ b/heap-dump-service/docs/docs.go @@ -0,0 +1,137 @@ +// Code generated by swaggo/swag. DO NOT EDIT +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": {}, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/upload": { + "post": { + "description": "Request a new Signed Upload URL for a specific file", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "v1" + ], + "summary": "Get signed upload URL", + "parameters": [ + { + "description": "Request a new Signed Upload URL", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/SigningRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/SigningResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + } + } + } + }, + "definitions": { + "ErrorResponse": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + }, + "SigningRequest": { + "type": "object", + "properties": { + "filename": { + "type": "string", + "example": "test_file.dump" + }, + "namespace": { + "type": "string", + "example": "beacon" + }, + "tenant": { + "type": "string", + "example": "cloud-beacon" + } + } + }, + "SigningResponse": { + "type": "object", + "properties": { + "aes-key": { + "type": "string" + }, + "encrypted-aes-key": { + "type": "string" + }, + "encrypted-aes-key-url": { + "type": "string" + }, + "url": { + "type": "string" + } + } + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "v1", + Host: "", + BasePath: "/api/v1", + Schemes: []string{}, + Title: "Heap Dump Central Service", + Description: "Central Service to create presigned upload URLs to s3 buckets", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/heap-dump-service/docs/swagger.json b/heap-dump-service/docs/swagger.json new file mode 100644 index 0000000..c60e65f --- /dev/null +++ b/heap-dump-service/docs/swagger.json @@ -0,0 +1,111 @@ +{ + "swagger": "2.0", + "info": { + "contact": {} + }, + "basePath": "/api/v1", + "paths": { + "/upload": { + "post": { + "description": "Request a new Signed Upload URL for a specific file", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "v1" + ], + "summary": "Get signed upload URL", + "parameters": [ + { + "description": "Request a new Signed Upload URL", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/SigningRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/SigningResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + } + } + } + }, + "definitions": { + "ErrorResponse": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + }, + "SigningRequest": { + "type": "object", + "properties": { + "filename": { + "type": "string", + "example": "test_file.dump" + }, + "namespace": { + "type": "string", + "example": "beacon" + }, + "tenant": { + "type": "string", + "example": "cloud-beacon" + } + } + }, + "SigningResponse": { + "type": "object", + "properties": { + "aes-key": { + "type": "string" + }, + "encrypted-aes-key": { + "type": "string" + }, + "encrypted-aes-key-url": { + "type": "string" + }, + "url": { + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/heap-dump-service/docs/swagger.yaml b/heap-dump-service/docs/swagger.yaml new file mode 100644 index 0000000..6609f36 --- /dev/null +++ b/heap-dump-service/docs/swagger.yaml @@ -0,0 +1,72 @@ +basePath: /api/v1 +definitions: + ErrorResponse: + properties: + error: + type: string + type: object + SigningRequest: + properties: + filename: + example: test_file.dump + type: string + namespace: + example: beacon + type: string + tenant: + example: cloud-beacon + type: string + type: object + SigningResponse: + properties: + aes-key: + type: string + encrypted-aes-key: + type: string + encrypted-aes-key-url: + type: string + url: + type: string + type: object +info: + contact: {} +paths: + /upload: + post: + consumes: + - application/json + description: Request a new Signed Upload URL for a specific file + parameters: + - description: Request a new Signed Upload URL + in: body + name: request + required: true + schema: + $ref: '#/definitions/SigningRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/SigningResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/ErrorResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/ErrorResponse' + summary: Get signed upload URL + tags: + - v1 +swagger: "2.0" diff --git a/heap-dump-service/go.mod b/heap-dump-service/go.mod new file mode 100644 index 0000000..76c4aaa --- /dev/null +++ b/heap-dump-service/go.mod @@ -0,0 +1,144 @@ +module github.com/dbschenker/heap-dump-management/heap-dump-service + +go 1.23.0 + +toolchain go1.23.3 + +require ( + github.com/aws/aws-sdk-go v1.55.5 + github.com/docker/go-connections v0.5.0 + github.com/gin-gonic/gin v1.10.0 + github.com/hashicorp/vault/api v1.15.0 + github.com/mittwald/vaultgo v0.1.9 + github.com/prometheus/client_golang v1.20.5 + github.com/shaj13/go-guardian/v2 v2.11.6 + github.com/shaj13/libcache v1.2.1 + github.com/sirupsen/logrus v1.9.3 + github.com/swaggo/files v1.0.1 + github.com/swaggo/gin-swagger v1.6.0 + github.com/swaggo/swag v1.16.4 + github.com/testcontainers/testcontainers-go v0.34.0 +) + +require ( + github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 // indirect + github.com/containerd/platforms v0.2.1 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/sys/userns v0.1.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/stretchr/testify v1.10.0 // indirect + github.com/x448/float16 v0.8.4 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 // indirect + go.opentelemetry.io/otel/sdk v1.33.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) + +require ( + dario.cat/mergo v1.0.1 // indirect + github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/bytedance/sonic v1.12.6 // indirect + github.com/bytedance/sonic/loader v0.2.1 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/docker v27.4.0+incompatible // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/gabriel-vasile/mimetype v1.4.7 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-jose/go-jose/v4 v4.0.4 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/spec v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.23.0 // indirect + github.com/goccy/go-json v0.10.4 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-retryablehttp v0.7.7 // indirect + github.com/hashicorp/go-rootcerts v1.0.2 // indirect + github.com/hashicorp/go-secure-stdlib/parseutil v0.1.8 // indirect + github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect + github.com/hashicorp/go-sockaddr v1.0.7 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/hashicorp/vault/api/auth/kubernetes v0.8.0 + 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.17.11 // indirect + github.com/klauspost/cpuid/v2 v2.2.9 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect + github.com/magiconair/properties v1.8.9 // indirect + github.com/mailru/easyjson v0.9.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/sys/sequential v0.6.0 // indirect + github.com/moby/sys/user v0.3.0 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.61.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/ryanuber/go-glob v1.0.0 // indirect + github.com/shirou/gopsutil/v3 v3.24.5 // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/tklauser/go-sysconf v0.3.14 // indirect + github.com/tklauser/numcpus v0.9.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect + go.opentelemetry.io/otel v1.33.0 // indirect + go.opentelemetry.io/otel/metric v1.33.0 // indirect + go.opentelemetry.io/otel/trace v1.33.0 // indirect + golang.org/x/arch v0.12.0 // indirect + golang.org/x/crypto v0.31.0 // indirect + golang.org/x/net v0.32.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect + golang.org/x/time v0.8.0 // indirect + golang.org/x/tools v0.28.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect + google.golang.org/grpc v1.69.0 // indirect + google.golang.org/protobuf v1.35.2 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/api v0.32.0 // indirect + k8s.io/apimachinery v0.32.0 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/utils v0.0.0-20241210054802-24370beab758 // indirect + sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.5.0 // indirect +) diff --git a/heap-dump-service/go.sum b/heap-dump-service/go.sum new file mode 100644 index 0000000..f9f0e7b --- /dev/null +++ b/heap-dump-service/go.sum @@ -0,0 +1,499 @@ +cloud.google.com/go v0.16.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU= +github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bradfitz/gomemcache v0.0.0-20170208213004-1952afaa557d/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60= +github.com/bytedance/sonic v1.12.6 h1:/isNmCUF2x3Sh8RAp/4mh4ZGkcFAX/hLrzrK3AvpRzk= +github.com/bytedance/sonic v1.12.6/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E= +github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v27.4.0+incompatible h1:I9z7sQ5qyzO0BfAb9IMOawRkAGxhYsidKiTMcm0DU+A= +github.com/docker/docker v27.4.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= +github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/evanphx/json-patch v0.0.0-20200808040245-162e5629780b/go.mod h1:NAJj0yf/KaRKURN6nyi7A9IZydMivZEm9oQLWNjfKDc= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fsnotify/fsnotify v1.4.3-0.20170329110642-4da3e2cfbabc/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/gabriel-vasile/mimetype v1.4.7 h1:SKFKl7kD0RiPdbht0s7hFtjl489WcQ1VyPW8ZzUMYCA= +github.com/gabriel-vasile/mimetype v1.4.7/go.mod h1:GDlAgAyIRT27BhFl53XNAFtfjzOkLaF35JdEG0P7LtU= +github.com/garyburd/redigo v1.1.1-0.20170914051019-70e1b1943d4f/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= +github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= +github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-asn1-ber/asn1-ber v1.5.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= +github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E= +github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc= +github.com/go-ldap/ldap/v3 v3.2.4/go.mod h1:iYS1MdmrmceOJ1QOTnRXrIs7i3kloqtmGQjRvjKpyMg= +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= +github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY= +github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= +github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o= +github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw= +github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= +github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f/go.mod h1:ijRvpgDJDI262hYq/IQVYgf8hd8IHUs93Ol0kvMBAx4= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/lint v0.0.0-20170918230701-e5d664eb928e/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= +github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +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/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.1.1-0.20171103154506-982329095285/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= +github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/googleapis/gnostic v0.1.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/gregjones/httpcache v0.0.0-20170920190843-316c5e0ff04e/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= +github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= +github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-secure-stdlib/parseutil v0.1.8 h1:iBt4Ew4XEGLfh6/bPk4rSYmuZJGizr6/x/AEizP0CQc= +github.com/hashicorp/go-secure-stdlib/parseutil v0.1.8/go.mod h1:aiJI+PIApBRQG7FZTEBx5GiiX+HbOHilUdNxUZi4eV0= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= +github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw= +github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v0.0.0-20170914154624-68e816d1c783/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/vault/api v1.15.0 h1:O24FYQCWwhwKnF7CuSqP30S51rTV7vz1iACXE/pj5DA= +github.com/hashicorp/vault/api v1.15.0/go.mod h1:+5YTO09JGn0u+b6ySD/LLVf8WkJCPLAL2Vkmrn2+CM8= +github.com/hashicorp/vault/api/auth/kubernetes v0.8.0 h1:6jPcORq7OHwf+MCbaaUmiBvMhETAaZ7+i97WfZtF5kc= +github.com/hashicorp/vault/api/auth/kubernetes v0.8.0/go.mod h1:nfl5sRUUork0ZSfV3xf+pgAFQSD5kSkL0k9axg523DM= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/inconshreveable/log15 v0.0.0-20170622235902-74a0988b5f80/go.mod h1:cOaXtrgN4ScfRrD9Bre7U1thNq5RtJ8ZoP4iXVGRj6o= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= +github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0= +github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k= +github.com/magiconair/properties v1.7.4-0.20170902060319-8d7837e64d3c/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM= +github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/mattn/go-colorable v0.0.10-0.20170816031813-ad5389df28cd/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.2/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v0.0.0-20170523030023-d0303fe80992/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mittwald/vaultgo v0.1.9 h1:IVWYoxGmI75w8gZM8Y7z9FsNI2/dwd5dJs13VtOo+Vc= +github.com/mittwald/vaultgo v0.1.9/go.mod h1:MuFKjvIXDjRU8cVxAKS/12JcxxzRCWzbdDcPC8sGdQQ= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo= +github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +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/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/pelletier/go-toml v1.0.1-0.20170904195809-1d6b12b7cb29/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= +github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.61.0 h1:3gv/GThfX0cV2lpO7gkTUwZru38mxevy90Bj8YFSRQQ= +github.com/prometheus/common v0.61.0/go.mod h1:zr29OCN/2BsJRaFwG8QOBr41D6kkchKbpeNH7pAjb/s= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= +github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= +github.com/shaj13/go-guardian/v2 v2.11.6 h1:N0UgnL+AI0IH59eii0H0QnQEesyPPmGFB1h9g1MkZ8g= +github.com/shaj13/go-guardian/v2 v2.11.6/go.mod h1:rSe5VLuWu9EyUT68Xi6qxb/DJc+ajiqPAq+VKhEUKkE= +github.com/shaj13/libcache v1.0.0/go.mod h1:YCq92Zosqj4erhlLdm2Mu1cX2FDAxjfFOxTphzN7S9U= +github.com/shaj13/libcache v1.2.1 h1:ET4FBxwUJhNVDD/EMOUIG97AQVktlkc//SPAga5JF4c= +github.com/shaj13/libcache v1.2.1/go.mod h1:YCq92Zosqj4erhlLdm2Mu1cX2FDAxjfFOxTphzN7S9U= +github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= +github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= +github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/afero v0.0.0-20170901052352-ee1bd8ee15a1/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.1.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg= +github.com/spf13/jwalterweatherman v0.0.0-20170901151539-12bd96e66386/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.1-0.20170901120850-7aff26db30c1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.0.0/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM= +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= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= +github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= +github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M= +github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo= +github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A= +github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg= +github.com/testcontainers/testcontainers-go v0.34.0 h1:5fbgF0vIN5u+nD3IWabQwRybuB4GY8G2HHgCkbMzMHo= +github.com/testcontainers/testcontainers-go v0.34.0/go.mod h1:6P/kMkQe8yqPHfPWNulFGdFHTD8HB2vLq/231xY2iPQ= +github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU= +github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY= +github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPDo= +github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS604NSRyI= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/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= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= +go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw= +go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 h1:wpMfgF8E1rkrT1Z6meFh1NDtownE9Ii3n3X2GJYjsaU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0= +go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ= +go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M= +go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM= +go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM= +go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s= +go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck= +go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg= +go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY= +golang.org/x/arch v0.12.0 h1:UsYJhbzPYGsT0HbEdmYcqtCv8UNGvnaL561NnIUvaKg= +golang.org/x/arch v0.12.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +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-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +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.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +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/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= +golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +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-20191004110552-13f9640d40b9/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-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-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= +golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= +golang.org/x/oauth2 v0.0.0-20170912212905-13449ad91cb2/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20170517211232-f52d1811a629/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-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/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/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-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/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-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/time v0.0.0-20170424234030-8be79e1e0910/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= +golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +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.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8= +golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw= +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/api v0.0.0-20170921000349-586095a6e407/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20170918111702-1e559d0a00ee/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q= +google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 h1:8ZmaLZE4XWrtU3MyClkYqqtl6Oegr3235h7jxsDyqCY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= +google.golang.org/grpc v1.2.1-0.20170921194603-d4b75ebd4f9f/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.69.0 h1:quSiOM1GJPmPH5XtU+BCoVXcDVJJAzNcoyfC2cCjGkI= +google.golang.org/grpc v1.69.0/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= +google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= +google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/go-jose/go-jose.v2 v2.6.3/go.mod h1:zzZDPkNNw/c9IE7Z9jr11mBZQhKQTMzoEEIoEdZlFBI= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= +k8s.io/api v0.18.8/go.mod h1:d/CXqwWv+Z2XEG1LgceeDmHQwpUJhROPx16SlxJgERY= +k8s.io/api v0.32.0 h1:OL9JpbvAU5ny9ga2fb24X8H6xQlVp+aJMFlgtQjR9CE= +k8s.io/api v0.32.0/go.mod h1:4LEwHZEf6Q/cG96F3dqR965sYOfmPM7rq81BLgsE0p0= +k8s.io/apimachinery v0.18.8/go.mod h1:6sQd+iHEqmOtALqOFjSWp2KZ9F0wlU/nWm0ZgsYWMig= +k8s.io/apimachinery v0.32.0 h1:cFSE7N3rmEEtv4ei5X6DaJPHHX0C+upp+v5lVPiEwpg= +k8s.io/apimachinery v0.32.0/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= +k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20200410145947-61e04a5be9a6/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E= +k8s.io/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJJI8IUa1AmH/qa0= +k8s.io/utils v0.0.0-20241210054802-24370beab758/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/structured-merge-diff/v3 v3.0.0-20200116222232-67a7b8c61874/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw= +sigs.k8s.io/structured-merge-diff/v3 v3.0.0/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw= +sigs.k8s.io/structured-merge-diff/v4 v4.5.0 h1:nbCitCK2hfnhyiKo6uf2HxUPTCodY6Qaf85SbDIaMBk= +sigs.k8s.io/structured-merge-diff/v4 v4.5.0/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/heap-dump-service/internal/config/config.go b/heap-dump-service/internal/config/config.go new file mode 100644 index 0000000..4cd5e53 --- /dev/null +++ b/heap-dump-service/internal/config/config.go @@ -0,0 +1,64 @@ +package config + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "strings" + + log "github.com/sirupsen/logrus" +) + +type AppConfig struct { + Metrics struct { + Port int + Path string + } + App struct { + Port int + Bucket string + } + Vault struct { + VaultTransitMount string + VaultRole string + VaultAuthMountPath string + } + ServiceAccount struct { + JWTokenMountPoint string + } +} + +func LoadConfigFromEnvironment(envVarName string) (AppConfig, error) { + configFile, found := os.LookupEnv(envVarName) + var appConfig AppConfig + if !found { + log.WithFields(log.Fields{ + "caller": "LoadConfigFromEnvironment", + }).Warnf(fmt.Sprintf("Environment variable for config file not set: %s", envVarName)) + return appConfig, errors.New(fmt.Sprintf("Environment variable for config file not set: %s", envVarName)) + } + return LoadConfigFromFile(configFile) +} + +func LoadConfigFromFile(configFile string) (AppConfig, error) { + jsonData, err := os.ReadFile(configFile) + var appConfig AppConfig + if err != nil { + log.WithFields(log.Fields{ + "caller": "LoadConfigFromEnvironment", + }).Warnf(fmt.Sprintf("Failed to load config file '%v': %v", configFile, err)) + return appConfig, errors.New(fmt.Sprintf("Failed to load config file '%v': %v", configFile, err.Error())) + } + + d := json.NewDecoder(strings.NewReader(string(jsonData))) + d.DisallowUnknownFields() + err = d.Decode(&appConfig) + if err != nil { + log.WithFields(log.Fields{ + "caller": "LoadConfigFromEnvironment", + }).Warnf(fmt.Sprintf("Failed to parse json data of file '%v': %v", configFile, err)) + return appConfig, errors.New(fmt.Sprintf("Failed to parse json data of file '%v': %v", configFile, err.Error())) + } + return appConfig, nil +} diff --git a/heap-dump-service/internal/config/config_test.go b/heap-dump-service/internal/config/config_test.go new file mode 100644 index 0000000..e540f93 --- /dev/null +++ b/heap-dump-service/internal/config/config_test.go @@ -0,0 +1,105 @@ +package config + +import ( + "errors" + "fmt" + "os" + "reflect" + "testing" +) + +var Want = AppConfig{ + Metrics: struct { + Port int + Path string + }{ + Port: 8081, + Path: "/metrics", + }, + App: struct { + Port int + Bucket string + }{ + Port: 8080, + Bucket: "test-bucket", + }, + Vault: struct { + VaultTransitMount string + VaultRole string + VaultAuthMountPath string + }{ + VaultTransitMount: "eaas-heap-dump-service", + VaultRole: "heap-dump-service", + VaultAuthMountPath: "kubernetes-toolbox-ref-np-kubernetes", + }, + ServiceAccount: struct { + JWTokenMountPoint string + }{ + JWTokenMountPoint: "/var/run/secrets/kubernetes.io/serviceaccount/token", + }, +} + +func TestLoadConfigFromFile(t *testing.T) { + got, err := LoadConfigFromFile("../../config/test/test-config.json") + if err != nil { + t.Errorf("Failed to construct config: %v", err) + } + if !reflect.DeepEqual(got, Want) { + t.Errorf("got %+v, want %+v", got, Want) + } +} + +func TestFailLoadConfigFromFile(t *testing.T) { + want := errors.New(fmt.Sprintf("Failed to load config file '%v': %v", "does-not-exist.json", "open does-not-exist.json: no such file or directory")) + _, got := LoadConfigFromFile("does-not-exist.json") + if got == nil { + t.Errorf("This should produce an error!") + } + if !reflect.DeepEqual(got.Error(), want.Error()) { + t.Errorf("got %+v, want %+v", got, want) + } + want = errors.New(fmt.Sprintf("Failed to parse json data of file '%v': %v", "../../config/test/bad-config.json", "json: unknown field \"invalid\"")) + _, got = LoadConfigFromFile("../../config/test/bad-config.json") + if got == nil { + t.Errorf("This should produce an error!") + } + if !reflect.DeepEqual(got.Error(), want.Error()) { + t.Errorf("got %+v, want %+v", got, want) + } +} + +func TestLoadConfigFromEnv(t *testing.T) { + os.Setenv("TEST_APP_CONFIG_FILE", "../../config/test/test-config.json") + defer os.Unsetenv("TEST_APP_CONFIG_FILE") + got, err := LoadConfigFromEnvironment("TEST_APP_CONFIG_FILE") + if err != nil { + t.Errorf("Failed to construct config: %v", err) + } + if !reflect.DeepEqual(got, Want) { + t.Errorf("got %+v, want %+v", got, Want) + } +} + +func TestFailLoadConfigFromEnvironment(t *testing.T) { + want := errors.New(fmt.Sprintf("Failed to load config file '%v': %v", "does-not-exist.json", "open does-not-exist.json: no such file or directory")) + os.Setenv("TEST_APP_CONFIG_FILE", "does-not-exist.json") + defer os.Unsetenv("TEST_APP_CONFIG_FILE") + _, got := LoadConfigFromEnvironment("TEST_APP_CONFIG_FILE") + if got == nil { + t.Errorf("This should produce an error!") + } + if !reflect.DeepEqual(got.Error(), want.Error()) { + t.Errorf("got %+v, want %+v", got, want) + } +} + +func TestFailNoConfigEnv(t *testing.T) { + want := errors.New(fmt.Sprintf("Environment variable for config file not set: %s", "TEST_NO_ENV")) + _, got := LoadConfigFromEnvironment("TEST_NO_ENV") + if got == nil { + t.Errorf("This should produce an error!") + } + if !reflect.DeepEqual(got.Error(), want.Error()) { + t.Errorf("got %+v, want %+v", got, want) + } +} diff --git a/heap-dump-service/internal/logging/logging.go b/heap-dump-service/internal/logging/logging.go new file mode 100644 index 0000000..05a3cbb --- /dev/null +++ b/heap-dump-service/internal/logging/logging.go @@ -0,0 +1,51 @@ +package logging + +import ( + "os" + "time" + + "github.com/gin-gonic/gin" + log "github.com/sirupsen/logrus" +) + +func SetupLogging() { + log.SetFormatter(&log.JSONFormatter{}) + log.SetOutput(os.Stdout) + log.SetLevel(log.InfoLevel) +} + +func GetDurationInMillseconds(start time.Time) float64 { + end := time.Now() + duration := end.Sub(start) + milliseconds := float64(duration) / float64(time.Millisecond) + rounded := float64(int(milliseconds*100+.5)) / 100 + return rounded +} + +func JSONLogMiddleware() gin.HandlerFunc { + log.SetFormatter(&log.JSONFormatter{}) + return func(c *gin.Context) { + // Start timer + //start := time.Now() + + // Process Request + c.Next() + + // Stop timer + //duration := GetDurationInMillseconds(start) + + entry := log.WithFields(log.Fields{ + "caller": c.FullPath(), + "method": c.Request.Method, + "path": c.Request.RequestURI, + "status": c.Writer.Status(), + "referrer": c.Request.Referer(), + }) + + if c.Writer.Status() >= 500 { + entry.Error(c.Errors.String()) + } else { + entry.Info("") + } + } +} diff --git a/heap-dump-service/internal/logging/logging_test.go b/heap-dump-service/internal/logging/logging_test.go new file mode 100644 index 0000000..63182c0 --- /dev/null +++ b/heap-dump-service/internal/logging/logging_test.go @@ -0,0 +1,31 @@ +package logging + +import ( + "os" + "reflect" + "testing" + "time" + + log "github.com/sirupsen/logrus" +) + +func TestSetupLogging(t *testing.T) { + SetupLogging() + if log.GetLevel() != log.InfoLevel { + t.Errorf("got %+v, want %+v", log.GetLevel(), log.InfoLevel) + } + if !reflect.DeepEqual(log.StandardLogger().Formatter, &log.JSONFormatter{}) { + t.Errorf("got %+v, want %+v", log.StandardLogger().Formatter, &log.JSONFormatter{}) + } + if !reflect.DeepEqual(log.StandardLogger().Out, os.Stdout) { + t.Errorf("got %+v, want %+v", log.StandardLogger().Out, os.Stdout) + } +} + +func TestDuration(t *testing.T) { + start := time.Now() + duration := GetDurationInMillseconds(start) + if duration != 0 { + t.Errorf("Duration Calculation took too long! %v", duration) + } +} diff --git a/heap-dump-service/internal/metrics/metrics.go b/heap-dump-service/internal/metrics/metrics.go new file mode 100644 index 0000000..7610453 --- /dev/null +++ b/heap-dump-service/internal/metrics/metrics.go @@ -0,0 +1,28 @@ +package metrics + +import ( + "fmt" + "net/http" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +var HeapDumpHandled = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "handled_heap_dumps", + Namespace: "heap_dump_service", + Help: "Number of handled heap dumps", + }, + []string{"namespace", "tenant"}, +) + +func init() { + prometheus.MustRegister(HeapDumpHandled) +} + +func StartMetricServer(port int, path string) { + http.Handle(path, promhttp.Handler()) + hostAddress := fmt.Sprintf(":%v", port) + http.ListenAndServe(hostAddress, nil) +} diff --git a/heap-dump-service/internal/metrics/metrics_test.go b/heap-dump-service/internal/metrics/metrics_test.go new file mode 100644 index 0000000..fb58bc6 --- /dev/null +++ b/heap-dump-service/internal/metrics/metrics_test.go @@ -0,0 +1,29 @@ +package metrics + +import ( + "net/http" + "strings" + "testing" + "time" +) + +func TestPrometheusServerStartup(t *testing.T) { + + go func() { + StartMetricServer(21338, "/metrics") + }() + + time.Sleep(1000 * time.Millisecond) + + request, _ := http.NewRequest(http.MethodGet, "http://localhost:21338/metrics", strings.NewReader("")) + resp, err := http.DefaultClient.Do(request) + + if err != nil { + t.Errorf("Metrics endpoint did not start: %v", err) + } + + if resp.StatusCode != http.StatusOK { + t.Errorf("Want status '%d', got '%d'", http.StatusOK, resp.StatusCode) + } + +} diff --git a/heap-dump-service/internal/rest-api/auth/sa-auth.go b/heap-dump-service/internal/rest-api/auth/sa-auth.go new file mode 100644 index 0000000..2d2bf5a --- /dev/null +++ b/heap-dump-service/internal/rest-api/auth/sa-auth.go @@ -0,0 +1,85 @@ +package auth + +import ( + "crypto/tls" + "errors" + "fmt" + "net/http" + "os" + "time" + + "github.com/dbschenker/heap-dump-management/heap-dump-service/internal/config" + "github.com/shaj13/go-guardian/v2/auth" + "github.com/shaj13/go-guardian/v2/auth/strategies/kubernetes" + "github.com/shaj13/libcache" + _ "github.com/shaj13/libcache/fifo" + log "github.com/sirupsen/logrus" + + "github.com/gin-gonic/gin" +) + +func generateHttpClient() *http.Client { + var tlsConfig tls.Config + + tlsConfig.InsecureSkipVerify = true + + t := http.DefaultTransport.(*http.Transport).Clone() + t.TLSClientConfig = &tlsConfig + t.MaxIdleConnsPerHost = 100 + t.TLSHandshakeTimeout = 10 * time.Second + + httpClient := &http.Client{ + Transport: t, + } + return httpClient +} + +func readTokenFromFile(filepath string) (string, error) { + jwt, err := os.ReadFile(filepath) + if err != nil { + return "", errors.New(fmt.Sprintf("Unable to read file containing service account token: %s", err.Error())) + } + return string(jwt), nil +} + +func setupGoGuardian(token string) auth.Strategy { + cacheObj := libcache.FIFO.NewUnsafe(10) + cacheObj.SetTTL(time.Minute * 5) + kubAuthAddr := kubernetes.SetAddress("https://kubernetes.default.svc") + httpClient := generateHttpClient() + kubeClientConfig := kubernetes.SetHTTPClient(httpClient) + authToken := kubernetes.SetServiceAccountToken(token) + return kubernetes.New(cacheObj, kubAuthAddr, kubeClientConfig, authToken) +} + +func SaAuth(c *gin.Context) { + log.WithFields(log.Fields{ + "caller": "SaAuth", + }).Info("Handling request") + + cfg := c.MustGet("cfg").(*config.AppConfig) + + token, err := readTokenFromFile(cfg.ServiceAccount.JWTokenMountPoint) + if err != nil { + log.WithFields(log.Fields{ + "caller": "SaAuth", + }).Errorf("Authentication Failure: %s", err.Error()) + c.JSON(http.StatusForbidden, gin.H{"error": "Authentication Failure"}) + c.Abort() + c.Writer.Header().Set("WWW-Authenticate", "Basic realm=Restricted") + return + } + + strategy := setupGoGuardian(token) + + _, err = strategy.Authenticate(c, c.Request) + if err != nil { + log.WithFields(log.Fields{ + "caller": "SaAuth", + }).Errorf("Authentication Failure: %s", err.Error()) + c.JSON(http.StatusForbidden, gin.H{"error": "Authentication Failure"}) + c.Abort() + c.Writer.Header().Set("WWW-Authenticate", "Basic realm=Restricted") + return + } +} diff --git a/heap-dump-service/internal/rest-api/auth/sa-auth_test.go b/heap-dump-service/internal/rest-api/auth/sa-auth_test.go new file mode 100644 index 0000000..35cd817 --- /dev/null +++ b/heap-dump-service/internal/rest-api/auth/sa-auth_test.go @@ -0,0 +1,14 @@ +package auth + +import ( + "testing" + + _ "github.com/shaj13/libcache/fifo" +) + +func TestGoGuardianSetup(t *testing.T) { + got := setupGoGuardian("test_token") + if got == nil { + t.Errorf("Could not build authentication strategy") + } +} diff --git a/heap-dump-service/internal/rest-api/requests/health.go b/heap-dump-service/internal/rest-api/requests/health.go new file mode 100644 index 0000000..8758f62 --- /dev/null +++ b/heap-dump-service/internal/rest-api/requests/health.go @@ -0,0 +1,35 @@ +package requests + +import ( + "fmt" + "net/http" + + "github.com/dbschenker/heap-dump-management/heap-dump-service/internal/config" + "github.com/dbschenker/heap-dump-management/heap-dump-service/internal/rest-api/utils" + "github.com/gin-gonic/gin" + log "github.com/sirupsen/logrus" +) + +func Health(c *gin.Context) { + cfg := c.MustGet("cfg").(*config.AppConfig) + client, err := utils.GenerateVaultClient(cfg.Vault.VaultRole, cfg.Vault.VaultAuthMountPath, cfg.ServiceAccount.JWTokenMountPoint) + if err != nil { + log.WithFields(log.Fields{ + "caller": "Health", + }).Fatalf(fmt.Sprintf("unable to initialize Vanilla Vault Client : %s", err.Error())) + } + + err = utils.CheckAWSAccess() + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + } + err = utils.CheckVaultAccess(client) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + } + c.JSON(http.StatusOK, gin.H{"status": "ok"}) +} + +func Liveness(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"status": "ok"}) +} diff --git a/heap-dump-service/internal/rest-api/requests/v1/signed-url.go b/heap-dump-service/internal/rest-api/requests/v1/signed-url.go new file mode 100644 index 0000000..704ff33 --- /dev/null +++ b/heap-dump-service/internal/rest-api/requests/v1/signed-url.go @@ -0,0 +1,167 @@ +package v1 + +import ( + "fmt" + "net/http" + "strings" + "time" + + log "github.com/sirupsen/logrus" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/s3" + "github.com/dbschenker/heap-dump-management/heap-dump-service/internal/config" + "github.com/dbschenker/heap-dump-management/heap-dump-service/internal/metrics" + "github.com/dbschenker/heap-dump-management/heap-dump-service/internal/rest-api/utils" + "github.com/gin-gonic/gin" +) + +type SigningRequest struct { + Tenant string `json:"tenant" example:"cloud-beacon"` + Namespace string `json:"namespace" example:"beacon"` + FileName string `json:"filename" example:"test_file.dump"` +} // @name SigningRequest + +type SigningResponse struct { + URL string `json:"url"` + EncryptedAesKey string `json:"encrypted-aes-key"` + EncryptedAesKeyURL string `json:"encrypted-aes-key-url"` + AesKey string `json:"aes-key"` +} // @name SigningResponse + +type ErrorResponse struct { + Error string `json:"error"` +} // @name ErrorResponse + +// @BasePath /api/v1 + +// @Summary Get signed upload URL +// @Schemes http https +// @Description Request a new Signed Upload URL for a specific file +// @Tags v1 +// @param request body SigningRequest true "Request a new Signed Upload URL" +// @Accept json +// @Produce json +// @securityDefinitions.apikey ApiKeyAuth +// @Success 200 {object} SigningResponse +// @Failure 400 {object} ErrorResponse +// @Failure 403 {object} ErrorResponse +// @Failure 404 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Router /upload [post] +func HandleRequestUpload(c *gin.Context) { + + cfg := c.MustGet("cfg").(*config.AppConfig) + vaultClient, err := utils.GenerateTransitVaultClient(cfg.Vault.VaultRole, cfg.Vault.VaultAuthMountPath, cfg.ServiceAccount.JWTokenMountPoint) + + if err != nil { + log.WithFields(log.Fields{ + "caller": "HandleRequestUpload", + }).Fatalf(fmt.Sprintf("unable to initialize Transit Vault Client : %s", err.Error())) + } + + var requestBody SigningRequest + if err := c.ShouldBindJSON(&requestBody); err != nil { + errResp := ErrorResponse{ + Error: fmt.Sprintf("Could not Unmarshal request body %s", err.Error()), + } + c.JSON(http.StatusBadRequest, errResp) + return + } + + objectKey := fmt.Sprintf("%s/%s/%s", requestBody.Tenant, requestBody.Namespace, requestBody.FileName) + aesKeyObjectKey := fmt.Sprintf("%s/%s/%s.%s", requestBody.Tenant, requestBody.Namespace, requestBody.FileName, "key") + + awsClient, err := utils.GenerateS3Client(cfg.App.Bucket) + + if err != nil { + log.WithFields(log.Fields{ + "caller": "HandleRequestUpload", + }).Error(fmt.Sprintf("Error initializing the AWS awsClient: %s", err.Error())) + + errResp := ErrorResponse{ + Error: fmt.Sprintf("Error initializing the AWS awsClient: %s", err.Error()), + } + c.JSON(http.StatusInternalServerError, errResp) + return + } + log.WithFields(log.Fields{ + "caller": "HandleRequestUpload", + }).Info(fmt.Sprintf("Received request to presign PutObject for %s", objectKey)) + sdkReq, _ := awsClient.PutObjectRequest(&s3.PutObjectInput{ + Bucket: aws.String(cfg.App.Bucket), + Key: aws.String(objectKey), + }) + u, _, err := sdkReq.PresignRequest(15 * time.Minute) + + if err != nil { + log.WithFields(log.Fields{ + "caller": "HandleRequestUpload", + }).Error(fmt.Sprintf("Error Creating Signed URL: %s", err.Error())) + + errResp := ErrorResponse{ + Error: fmt.Sprintf("Error Creating Signed URL: %s", err.Error()), + } + c.JSON(http.StatusInternalServerError, errResp) + return + } + + aesKey, err := utils.GenerateRandomBytes(32) + + if err != nil { + log.WithFields(log.Fields{ + "caller": "HandleRequestUpload", + }).Error(fmt.Sprintf("Error generating password: %s", err.Error())) + errResp := ErrorResponse{ + Error: fmt.Sprintf("Error generating password: %s", err.Error()), + } + c.JSON(http.StatusInternalServerError, errResp) + return + } + + encodedAesKey := utils.EncodeKey(aesKey) + + encryptedAesKey, err := utils.TransitEncryptString(vaultClient, cfg.Vault.VaultTransitMount, requestBody.Tenant, encodedAesKey) + + if err != nil { + log.WithFields(log.Fields{ + "caller": "HandleRequestUpload", + }).Error(fmt.Sprintf("Error encrypting password: %s", err.Error())) + errResp := ErrorResponse{ + Error: fmt.Sprintf("Error encrypting password: %s", err.Error()), + } + c.JSON(http.StatusInternalServerError, errResp) + return + } + + sdkReq, _ = awsClient.PutObjectRequest(&s3.PutObjectInput{ + Bucket: aws.String(cfg.App.Bucket), + Key: aws.String(aesKeyObjectKey), + }) + aesKeyURL, _, err := sdkReq.PresignRequest(15 * time.Minute) + + if err != nil { + log.WithFields(log.Fields{ + "caller": "HandleRequestUpload", + }).Error(fmt.Sprintf("Error generating presigned upload URL: %s", err.Error())) + errResp := ErrorResponse{ + Error: fmt.Sprintf("Error generating presigned upload URL: %s", err.Error()), + } + c.JSON(http.StatusInternalServerError, errResp) + return + } + + resp := SigningResponse{ + URL: u, + EncryptedAesKey: encryptedAesKey, + EncryptedAesKeyURL: aesKeyURL, + AesKey: encodedAesKey, + } + + c.JSON(http.StatusOK, resp) + + namespaceString := strings.ReplaceAll(requestBody.Namespace, "-", "_") + + metrics.HeapDumpHandled.WithLabelValues(namespaceString, requestBody.Tenant).Inc() + +} diff --git a/heap-dump-service/internal/rest-api/routes.go b/heap-dump-service/internal/rest-api/routes.go new file mode 100644 index 0000000..67e3738 --- /dev/null +++ b/heap-dump-service/internal/rest-api/routes.go @@ -0,0 +1,44 @@ +package restapi + +import ( + "fmt" + + docs "github.com/dbschenker/heap-dump-management/heap-dump-service/docs" + "github.com/dbschenker/heap-dump-management/heap-dump-service/internal/config" + "github.com/dbschenker/heap-dump-management/heap-dump-service/internal/logging" + "github.com/dbschenker/heap-dump-management/heap-dump-service/internal/rest-api/auth" + "github.com/dbschenker/heap-dump-management/heap-dump-service/internal/rest-api/requests" + apiV1 "github.com/dbschenker/heap-dump-management/heap-dump-service/internal/rest-api/requests/v1" + "github.com/gin-gonic/gin" + + swaggerfiles "github.com/swaggo/files" + ginSwagger "github.com/swaggo/gin-swagger" +) + +const BASE_PATH = "/api/v1" +const UPLOAD_ENDPOINT = "/upload" + +func Serve(cfg *config.AppConfig) { + + docs.SwaggerInfo.BasePath = BASE_PATH + router := gin.New() + router.SetTrustedProxies([]string{"10.0.0.0/8"}) + + router.Use(logging.JSONLogMiddleware()) + router.Use(gin.Recovery()) + + router.Use(func(c *gin.Context) { + c.Set("cfg", cfg) + c.Next() + }) + + v1 := router.Group(BASE_PATH) + { + v1.POST(UPLOAD_ENDPOINT, auth.SaAuth, apiV1.HandleRequestUpload) + } + router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler)) + router.GET("/health", requests.Health) + router.GET("/liveness", requests.Liveness) + + router.Run(":" + fmt.Sprint(cfg.App.Port)) +} diff --git a/heap-dump-service/internal/rest-api/utils/aws-helper.go b/heap-dump-service/internal/rest-api/utils/aws-helper.go new file mode 100644 index 0000000..6a54984 --- /dev/null +++ b/heap-dump-service/internal/rest-api/utils/aws-helper.go @@ -0,0 +1,45 @@ +package utils + +import ( + "errors" + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/aws/endpoints" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/s3" + "github.com/aws/aws-sdk-go/service/s3/s3iface" + "github.com/aws/aws-sdk-go/service/s3/s3manager" + "github.com/aws/aws-sdk-go/service/sts" +) + +func CheckAWSAccess() error { + svc := sts.New(session.New()) + input := &sts.GetCallerIdentityInput{} + + _, err := svc.GetCallerIdentity(input) + if err != nil { + return errors.New(fmt.Sprintf("Error authenticating to AWS: %s", err.(awserr.Error).Message())) + } + return nil +} + +func GenerateS3Client(bucketName string) (s3iface.S3API, error) { + + cfg := aws.NewConfig(). + WithEC2MetadataDisableTimeoutOverride(true). + WithCredentialsChainVerboseErrors(true) + + sess := session.Must(session.NewSession(cfg)) + region, err := s3manager.GetBucketRegion(aws.BackgroundContext(), sess, bucketName, endpoints.EuCentral1RegionID) + if err != nil { + return nil, err + } + + s3Svc := s3.New(sess, &aws.Config{ + Region: aws.String(region), + }) + + return s3Svc, nil +} diff --git a/heap-dump-service/internal/rest-api/utils/encryption.go b/heap-dump-service/internal/rest-api/utils/encryption.go new file mode 100644 index 0000000..63501f1 --- /dev/null +++ b/heap-dump-service/internal/rest-api/utils/encryption.go @@ -0,0 +1,21 @@ +package utils + +import ( + "crypto/rand" + "encoding/base64" + "errors" + "fmt" +) + +func GenerateRandomBytes(n int) ([]byte, error) { + b := make([]byte, n) + _, err := rand.Read(b) + if err != nil { + return nil, errors.New(fmt.Sprintf("Could not generate random bytes %s", err.Error())) + } + return b, nil +} + +func EncodeKey(key []byte) string { + return base64.StdEncoding.EncodeToString(key) +} diff --git a/heap-dump-service/internal/rest-api/utils/encryption_test.go b/heap-dump-service/internal/rest-api/utils/encryption_test.go new file mode 100644 index 0000000..68ec017 --- /dev/null +++ b/heap-dump-service/internal/rest-api/utils/encryption_test.go @@ -0,0 +1,31 @@ +package utils + +import ( + "encoding/base64" + "testing" +) + +func TestGenerateRandomString(t *testing.T) { + randomBytes, err := GenerateRandomBytes(32) + rnd := EncodeKey(randomBytes) + if err != nil { + t.Errorf(err.Error()) + } + clearKey, err := base64.StdEncoding.DecodeString(rnd) + if err != nil { + t.Errorf(err.Error()) + } + if len(clearKey) != 32 { + t.Errorf("String is too short! Should be %d, is %d", 32, len(rnd)) + } +} + +func TestGenerateRandomBytes(t *testing.T) { + rnd, err := GenerateRandomBytes(32) + if err != nil { + t.Errorf(err.Error()) + } + if len(rnd) != 32 { + t.Errorf("bytes is too short! Should be %d, is %d", 32, len(rnd)) + } +} diff --git a/heap-dump-service/internal/rest-api/utils/vault-helper.go b/heap-dump-service/internal/rest-api/utils/vault-helper.go new file mode 100644 index 0000000..57a8400 --- /dev/null +++ b/heap-dump-service/internal/rest-api/utils/vault-helper.go @@ -0,0 +1,118 @@ +package utils + +import ( + "context" + "errors" + "fmt" + "net/url" + "os" + + "github.com/hashicorp/vault/api" + vault "github.com/hashicorp/vault/api" + vaultAuth "github.com/hashicorp/vault/api/auth/kubernetes" + vaultTransit "github.com/mittwald/vaultgo" + log "github.com/sirupsen/logrus" +) + +func CheckVaultAccess(client *vault.Client) error { + + u, err := url.Parse(client.Address() + "v1/auth/token/lookup-self") + if err != nil { + return errors.New(fmt.Sprintf("Could not construct URL from client config: %s", err.Error())) + } + + request := vault.Request{ + Method: "GET", + URL: u, + ClientToken: client.Token(), + } + _, err = client.RawRequest(&request) + + if err != nil { + return errors.New(fmt.Sprintf("Unable to Access Vault: %s - %s", err.Error(), u.String())) + } + + return nil +} + +func GenerateVaultClient(role string, mountPath string, jwtLocation string) (*api.Client, error) { + config := api.DefaultConfig() + + k8sAuth, err := vaultAuth.NewKubernetesAuth( + role, + vaultAuth.WithServiceAccountTokenPath(jwtLocation), + vaultAuth.WithMountPath(mountPath), + ) + if err != nil { + log.WithFields(log.Fields{ + "caller": "GenerateVaultClient", + }).Fatalf(fmt.Sprintf("unable to initialize Vault Authentication : %s", err.Error())) + } + + vanillaVaultclient, err := api.NewClient(config) + if err != nil { + log.WithFields(log.Fields{ + "caller": "GenerateVaultClient", + }).Warn(fmt.Sprintf("unable to initialize Vanilla Vault Client : %s", err.Error())) + return nil, errors.New(fmt.Sprintf("unable to initialize Vanilla Vault Client : %s", err.Error())) + } + authInfo, err := vanillaVaultclient.Auth().Login(context.Background(), k8sAuth) + if err != nil { + log.WithFields(log.Fields{ + "caller": "GenerateVaultClient", + }).Warn(fmt.Sprintf("unable to log in with Kubernetes auth : %s", err.Error())) + return nil, errors.New(fmt.Sprintf("unable to log in with Kubernetes auth: %s", err.Error())) + } + if authInfo == nil { + log.WithFields(log.Fields{ + "caller": "GenerateVaultClient", + }).Warn(fmt.Sprintf("no auth info was returned after login")) + return nil, errors.New(fmt.Sprintf("no auth info was returned after login")) + } + return vanillaVaultclient, nil +} + +func GenerateTransitVaultClient(role string, mountPath string, jwtLocation string) (*vaultTransit.Client, error) { + + vaultURL, found := os.LookupEnv("VAULT_ADDR") + if !found { + log.WithFields(log.Fields{ + "caller": "GenerateTransitVaultClient", + }).Error(fmt.Sprintf("Could not find valid vault URL on env: %s", "VAULT_ADDR")) + return nil, errors.New(fmt.Sprintf("Could not find valid vault URL on env: %s", "VAULT_ADDR")) + } + + c, err := vaultTransit.NewClient( + vaultURL, + vaultTransit.WithCaPath(""), + vaultTransit.WithKubernetesAuth( + role, + vaultTransit.WithJwtFromFile(jwtLocation), + vaultTransit.WithMountPoint(mountPath), + ), + ) + if err != nil { + log.WithFields(log.Fields{ + "caller": "GenerateTransitVaultClient", + }).Error(fmt.Sprintf("Error creating Transit Vault Client: %s", err.Error())) + return nil, err + } + return c, nil +} + +func TransitEncryptString(client *vaultTransit.Client, mountPoint string, topicKey string, key string) (string, error) { + + transit := client.TransitWithMountPoint(mountPoint) + + encryptResponse, err := transit.Encrypt(topicKey, &vaultTransit.TransitEncryptOptions{ + Plaintext: key, + }) + if err != nil { + log.WithFields(log.Fields{ + "caller": "TransitEncryptString", + }).Error(fmt.Sprintf("Error occurred during encryption: %s", err.Error())) + return "", err + } + + return encryptResponse.Data.Ciphertext, nil +} diff --git a/heap-dump-service/internal/rest-api/utils/vault-helper_test.go b/heap-dump-service/internal/rest-api/utils/vault-helper_test.go new file mode 100644 index 0000000..5cd4584 --- /dev/null +++ b/heap-dump-service/internal/rest-api/utils/vault-helper_test.go @@ -0,0 +1,171 @@ +package utils + +import ( + "context" + "encoding/base64" + "fmt" + "os" + "testing" + "testing/fstest" + + "github.com/docker/go-connections/nat" + "github.com/hashicorp/vault/api" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" + + vaultTransit "github.com/mittwald/vaultgo" +) + +var Token = "test" + +var ValidFs = fstest.MapFS{ + "var/var/run/secrets/kubernetes.io/serviceaccount/token": {Data: []byte(Token)}, +} + +type VaultContainer struct { + container testcontainers.Container + mappedPort nat.Port + hostIP string + token string +} + +var Vault *VaultContainer + +func (v *VaultContainer) URI() string { + return fmt.Sprintf("http://%s:%s/", v.HostIP(), v.Port()) +} + +func (v *VaultContainer) Port() string { + return v.mappedPort.Port() +} + +func (v *VaultContainer) HostIP() string { + return v.hostIP +} + +func (v *VaultContainer) Token() string { + return v.token +} + +func (v *VaultContainer) Terminate(ctx context.Context) error { + return v.container.Terminate(ctx) +} + +func InitVaultContainer(ctx context.Context, version string) (*VaultContainer, error) { + port := nat.Port("8200/tcp") + + req := testcontainers.GenericContainerRequest{ + ProviderType: testcontainers.ProviderDocker, + ContainerRequest: testcontainers.ContainerRequest{ + Image: "vault:" + version, + ExposedPorts: []string{string(port)}, + WaitingFor: wait.ForListeningPort(port), + SkipReaper: true, + Env: map[string]string{ + "VAULT_ADDR": fmt.Sprintf("http://0.0.0.0:%s", port.Port()), + "VAULT_DEV_ROOT_TOKEN_ID": Token, + "VAULT_TOKEN": Token, + "VAULT_LOG_LEVEL": "trace", + }, + Cmd: []string{ + "server", + "-dev", + }, + Privileged: true, + }, + } + + v, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: req.ContainerRequest, + Started: true, + }) + if err != nil { + return nil, err + } + + vc := &VaultContainer{ + container: v, + mappedPort: "", + hostIP: "", + token: Token, + } + + vc.hostIP, err = v.Host(ctx) + if err != nil { + return nil, err + } + + vc.mappedPort, err = v.MappedPort(ctx, port) + if err != nil { + return nil, err + } + + _, _, err = vc.container.Exec(ctx, []string{ + "vault", + "secrets", + "enable", + "transit", + }) + if err != nil { + return nil, err + } + + return vc, nil +} + +func TestMain(m *testing.M) { + var err error + Vault, err = InitVaultContainer(context.Background(), "1.11.4") + if err != nil { + fmt.Printf("Could not start test container for vault: %s", err.Error()) + } + m.Run() +} + +func TestVaultEncryptString(t *testing.T) { + os.Setenv("VAULT_ADDR", Vault.URI()) + defer os.Unsetenv("VAULT_ADDR") + os.Setenv("VAULT_TOKEN", Vault.Token()) + defer os.Unsetenv("VAULT_TOKEN") + rnd, err := GenerateRandomBytes(32) + if err != nil { + t.Errorf(err.Error()) + } + + testClient, err := vaultTransit.NewClient(Vault.URI(), vaultTransit.WithCaPath(""), vaultTransit.WithAuthToken(Vault.token)) + if err != nil { + t.Errorf("Error creating test client: %s", err.Error()) + } + + ret, err := TransitEncryptString(testClient, "transit", "test-topic", base64.URLEncoding.EncodeToString(rnd)) + + if err != nil { + t.Errorf("Error encrypting string: %s", err.Error()) + } + + transit := testClient.TransitWithMountPoint("transit") + plainTestResponse, err := transit.Decrypt("test-topic", &vaultTransit.TransitDecryptOptions{ + Ciphertext: string(ret), + }) + + if plainTestResponse.Data.Plaintext != base64.URLEncoding.EncodeToString(rnd) { + t.Errorf("Decryption failed! want %v, got %v", string(rnd), plainTestResponse.Data.Plaintext) + } + +} + +func TestCheckVaultAccess(t *testing.T) { + os.Setenv("VAULT_ADDR", Vault.URI()) + defer os.Unsetenv("VAULT_ADDR") + os.Setenv("VAULT_TOKEN", Vault.Token()) + defer os.Unsetenv("VAULT_TOKEN") + config := api.DefaultConfig() + testClient, err := api.NewClient(config) + if err != nil { + t.Errorf("Could not initialize vault client: %s", err.Error()) + } + err = CheckVaultAccess(testClient) + if err != nil { + t.Errorf("Failed Health Check: %s", err.Error()) + } +} diff --git a/heap-dump-service/sonar-project.properties b/heap-dump-service/sonar-project.properties new file mode 100644 index 0000000..d2f9651 --- /dev/null +++ b/heap-dump-service/sonar-project.properties @@ -0,0 +1,11 @@ +sonar.projectKey=com.dbschenker.gilds.devops.heap-dump-management_heap_dump_service +sonar.projectName=Heap Dump Central Service + +sonar.sources=. +sonar.exclusions=**/*_test.go + +sonar.tests=. +sonar.test.inclusions=**/*_test.go + +sonar.go.coverage.reportPaths=coverage.out +sonar.go.tests.reportPaths=report.json \ No newline at end of file diff --git a/notify-sidecar/.dockerignore b/notify-sidecar/.dockerignore new file mode 100644 index 0000000..b5e8bdb --- /dev/null +++ b/notify-sidecar/.dockerignore @@ -0,0 +1,5 @@ +.venv/ +.vscode/ +test/ +*.sh +.git/ \ No newline at end of file diff --git a/notify-sidecar/.gitignore b/notify-sidecar/.gitignore new file mode 100644 index 0000000..36f971e --- /dev/null +++ b/notify-sidecar/.gitignore @@ -0,0 +1 @@ +bin/* diff --git a/notify-sidecar/.vscode/launch.json b/notify-sidecar/.vscode/launch.json new file mode 100644 index 0000000..7630a04 --- /dev/null +++ b/notify-sidecar/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch app", + "type": "go", + "request": "launch", + "mode": "auto", + "program":"${workspaceFolder}/cmd/app/main.go", + "env": {"APP_CONFIG_FILE": "${workspaceFolder}/config/local-config.json"}, + } + ] +} \ No newline at end of file diff --git a/notify-sidecar/Dockerfile b/notify-sidecar/Dockerfile new file mode 100644 index 0000000..14040e6 --- /dev/null +++ b/notify-sidecar/Dockerfile @@ -0,0 +1,11 @@ +FROM golang:1.22 AS build + +WORKDIR /go/src/app +COPY . . + +RUN go mod download +RUN CGO_ENABLED=0 go build -o /go/bin/app cmd/app/main.go + +FROM gcr.io/distroless/static-debian11 +COPY --from=build /go/bin/app / +CMD ["/app"] \ No newline at end of file diff --git a/notify-sidecar/README.md b/notify-sidecar/README.md new file mode 100644 index 0000000..6561a02 --- /dev/null +++ b/notify-sidecar/README.md @@ -0,0 +1,19 @@ +# Notify Sidecar + +This is the notify sidecar of the heap dump collection solution. A small sidecar that is responsible of reacting on actively written heap dumps. on completition of the write operation the sidecar will encrypt them and upload them to a central s3 bucket for a tenant. + +![](docs/Architecture.svg) + +## What it does + +As shown in the Architecture the notify sidecar is doing the actual encryption and upload of a heap dump file. It actively watches a shared volume if heap dumps are written to it and reacts on these. +Upon detection the notify sidecar will request a presigned upload URL to a central s3 bucket and an encryption key from the heap dump service. This key is encrypted with the transit key of the specific tenant. +After the heap dump has been written completly, the notify sidecar will encrypt it with the tenants key in AES-256 and upload it via the presigned upload URL. It will also upload the encrypted AES key next to the upload. +In order to decrypt and use the heap dump, please check the heap-dump-companion documentation. + +![](docs/diagram.svg) + +### Config and Setup + +see [config.md](docs/config.md) + diff --git a/notify-sidecar/cmd/app/main.go b/notify-sidecar/cmd/app/main.go new file mode 100644 index 0000000..384cc86 --- /dev/null +++ b/notify-sidecar/cmd/app/main.go @@ -0,0 +1,155 @@ +package main + +import ( + "container/list" + "encoding/base64" + "errors" + "fmt" + "io/fs" + "io/ioutil" + "os" + "path" + "strings" + "time" + + log "github.com/sirupsen/logrus" + + "github.com/dbschenker/heap-dump-management/notify-sidecar/internal/config" + "github.com/dbschenker/heap-dump-management/notify-sidecar/internal/logging" + "github.com/dbschenker/heap-dump-management/notify-sidecar/internal/metrics" + "github.com/dbschenker/heap-dump-management/notify-sidecar/internal/models" + "github.com/dbschenker/heap-dump-management/notify-sidecar/internal/utils" +) + +func handleNewHeapDump(fileSystem fs.FS, cfg config.AppConfig, file string) error { + response := new(models.SigningResponse) + err := utils.RequestUploadConfig(fileSystem, cfg, file, response) + if err != nil { + return errors.New(fmt.Sprintf("Error requesting upload URL: %s", err.Error())) + } + key, err := base64.StdEncoding.DecodeString(response.AesKey) + if err != nil { + return errors.New(fmt.Sprintf("Error decoding aes key: %s", err.Error())) + } + entryptedFileLocation, err := utils.EncryptDump(fileSystem, strings.TrimPrefix(file, "/"), key) + if err != nil { + return errors.New(fmt.Sprintf("Error encrypting dump: %s", err.Error())) + } + encryptDumpFileHandler, err := os.Open(entryptedFileLocation) + if err != nil { + return errors.New(fmt.Sprintf("Error reading %s: %s", entryptedFileLocation, err.Error())) + } + encryptedKeyFile, err := os.CreateTemp("/tmp", "key") + if err != nil { + return errors.New(fmt.Sprintf("Error creating tmp file %s: %s", encryptedKeyFile.Name(), err.Error())) + } + _, err = encryptedKeyFile.WriteString(response.EncryptedAesKey) + + if err != nil { + return errors.New(fmt.Sprintf("Error writing encrypted AesKey to file %s: %s", encryptedKeyFile.Name(), err.Error())) + } + err = utils.UploadToS3(response.URL, encryptDumpFileHandler) + if err != nil { + return err + } + + encryptedKeyFileHandler, err := os.Open(encryptedKeyFile.Name()) + + defer encryptedKeyFileHandler.Close() + defer os.Remove(encryptedKeyFile.Name()) + defer os.Remove(entryptedFileLocation) + defer os.Remove(file) + + if err != nil { + return errors.New(fmt.Sprintf("Error Creating FileHandler for %s: %s", encryptedKeyFile.Name(), err.Error())) + } + + err = utils.UploadToS3(response.EncryptedAesKeyURL, encryptedKeyFileHandler) + if err != nil { + return err + } + + log.WithFields(log.Fields{ + "caller": "handleNewHeapDump", + }).Info(fmt.Sprintf("Uploaded encrypted Heap dump for %s successfully", cfg.ServiceOwner.Tenant)) + + metrics.HeapDumpHandled.WithLabelValues(cfg.ServiceOwner.Tenant).Inc() + + return nil +} + +func cleanupStaleFiles(basePath string, staleFiles *list.List, appConfig config.AppConfig) { + for e := staleFiles.Front(); e != nil; e = e.Next() { + f := e.Value.(fs.FileInfo) + if time.Now().Sub(f.ModTime()) > time.Minute*5 { + log.WithFields(log.Fields{ + "caller": "pollChanges", + }).Info(fmt.Sprintf("Deleting %s", f.Name())) + os.Remove(path.Join(basePath, f.Name())) + staleFiles.Remove(e) + metrics.FailedDumps.WithLabelValues(appConfig.ServiceOwner.Tenant).Inc() + } + } +} + +func pollChanges(appConfig config.AppConfig) { + c := time.Tick(10 * time.Second) + var fzise int64 = 0 + staleFiles := list.New() + for range c { + log.WithFields(log.Fields{ + "caller": "pollChanges", + }).Info(fmt.Sprintf("Checking for new files at %s", time.Now())) + files, err := ioutil.ReadDir(appConfig.WatchPath.Path) + if err != nil { + log.WithFields(log.Fields{ + "caller": "pollChanges", + }).Fatal(fmt.Sprintf("Could not read files in dir: %s", err.Error())) + } + + for _, file := range files { + //defer os.Remove(path.Join(appConfig.WatchPath.Path, file.Name())) + log.WithFields(log.Fields{ + "caller": "pollChanges", + }).Debug(fmt.Sprintf("Checking Heap Dump: %s modified at %v, with size %d", file.Name(), file.ModTime(), file.Size())) + // Flag file for deletion + if time.Now().Sub(file.ModTime()) > time.Minute*1 { + log.WithFields(log.Fields{ + "caller": "pollChanges", + }).Info(fmt.Sprintf("Flagging %s for deletion", file.Name())) + staleFiles.PushBack(file) + } + // Ignore files smaller than 16 MB + if file.Size() < 16777216 { + log.WithFields(log.Fields{ + "caller": "pollChanges", + }).Debug(fmt.Sprintf("%s is too small for a Heap Dump: %d", file.Name(), file.Size())) + } else { + if time.Now().Sub(file.ModTime()) > time.Second*15 && fzise == file.Size() { + log.WithFields(log.Fields{ + "caller": "pollChanges", + }).Info(fmt.Sprintf("Processing file: %s modified at %v, with size %d", file.Name(), file.ModTime(), file.Size())) + err := handleNewHeapDump(os.DirFS("/"), appConfig, fmt.Sprintf("%s/%s", appConfig.WatchPath.Path, file.Name())) + utils.CheckError(err) + } + } + cleanupStaleFiles(appConfig.WatchPath.Path, staleFiles, appConfig) + log.WithFields(log.Fields{ + "caller": "pollChanges", + }).Debug(fmt.Sprintf("Write operation still in progress for Heap Dump %s", file.Name())) + fzise = file.Size() + time.Sleep(time.Second * 2) + } + } +} + +func main() { + logging.SetupLogging() + + appConfig, err := config.LoadConfigFromEnvironment("APP_CONFIG_FILE") + utils.CheckError(err) + + go pollChanges(appConfig) + + metrics.StartMetricServer(appConfig.Metrics.Port, appConfig.Metrics.Path) +} diff --git a/notify-sidecar/cmd/app/main_test.go b/notify-sidecar/cmd/app/main_test.go new file mode 100644 index 0000000..a5877be --- /dev/null +++ b/notify-sidecar/cmd/app/main_test.go @@ -0,0 +1,181 @@ +package main + +import ( + "context" + b64 "encoding/base64" + "encoding/json" + "errors" + "fmt" + "log" + "net/http" + "os" + "reflect" + "strings" + "testing" + "testing/fstest" + "time" + + cfg "github.com/dbschenker/heap-dump-management/notify-sidecar/internal/config" + "github.com/dbschenker/heap-dump-management/notify-sidecar/internal/models" +) + +var staticTestKey = b64.StdEncoding.EncodeToString([]byte{52, 74, 93, 7, 97, 74, 50, 186, 172, 14, 125, 208, 130, 218, 177, 215, 219, 219, 247, 163, 81, 86, 105, 60, 22, 162, 54, 81, 19, 37, 212, 49}) + +var staticBadTestKey = b64.StdEncoding.EncodeToString([]byte{52, 74}) + +var BadTestResponse = models.SigningResponse{ + URL: "http://test-url.org/upload", + EncryptedAesKey: "cryptedTest", + EncryptedAesKeyURL: "http://test-url.org/upload2", + AesKey: staticBadTestKey, +} + +var GoodTestResponse = models.SigningResponse{ + URL: "http://test-url.org/upload", + EncryptedAesKey: "cryptedTest", + EncryptedAesKeyURL: "http://test-url.org/upload2", + AesKey: staticTestKey, +} + +func cleanup(target string) { + os.Remove(target) +} + +func TestMain(m *testing.M) { + httpServer := http.Server{ + Addr: ":21347", + } + setup(&httpServer) + time.Sleep(1000 * time.Millisecond) + m.Run() + shutdown(&httpServer) +} + +func setup(serverPointer *http.Server) { + returnGoodJson, _ := json.Marshal(GoodTestResponse) + returnBadJson, _ := json.Marshal(BadTestResponse) + http.HandleFunc("/request/good", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, string(returnGoodJson)) + }) + http.HandleFunc("/request/bad", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, string(returnBadJson)) + }) + go func() { + if err := serverPointer.ListenAndServe(); err != http.ErrServerClosed { + log.Fatalf("HTTP server ListenAndServe Error: %v", err) + } + }() +} + +func shutdown(serverPointer *http.Server) { + if err := serverPointer.Shutdown(context.Background()); err != nil { + log.Printf("HTTP Server Shutdown Error: %v", err) + } +} + +func TestNoToken(t *testing.T) { + goodConfig := cfg.AppConfig{ + Metrics: struct { + Port int + Path string + }{ + Port: 8081, + Path: "/metrics", + }, + WatchPath: struct{ Path string }{ + Path: "/test", + }, + Middleware: struct{ Endpoint string }{ + Endpoint: "http://localhost:21347/request/good", + }, + ServiceOwner: struct { + Tenant string + }{ + Tenant: "testTenant", + }, + } + fs := fstest.MapFS{ + "test_heap_dump": {Data: []byte("asdfasdfasdf")}, + "var/run/secrets/kubernetes.io/serviceaccount/namespace": {Data: []byte("platform")}, + } + want := errors.New(fmt.Sprintf("Error requesting upload URL: %s", "Error reading SA Token: open var/run/secrets/kubernetes.io/serviceaccount/token: file does not exist")) + got := handleNewHeapDump(fs, goodConfig, "test_heap_dump") + if got == nil { + t.Errorf("This should produce an Error") + } + if !reflect.DeepEqual(got.Error(), want.Error()) { + t.Errorf("got %+v, want %+v", got, want) + } +} + +func TestBadAESKey(t *testing.T) { + badConfig := cfg.AppConfig{ + Metrics: struct { + Port int + Path string + }{ + Port: 8081, + Path: "/metrics", + }, + WatchPath: struct{ Path string }{ + Path: "/test", + }, + Middleware: struct{ Endpoint string }{ + Endpoint: "http://localhost:21347/request/bad", + }, + ServiceOwner: struct { + Tenant string + }{ + Tenant: "testTenant", + }, + } + fs := fstest.MapFS{ + "var/run/secrets/kubernetes.io/serviceaccount/token": {Data: []byte("test_token")}, + "var/run/secrets/kubernetes.io/serviceaccount/namespace": {Data: []byte("platform")}, + "test_heap_dump": {Data: []byte("dummy")}, + } + want := errors.New(fmt.Sprintf("Error encrypting dump: %s", "Error initializing ARE Cipher: crypto/aes: invalid key size 2")) + got := handleNewHeapDump(fs, badConfig, "test_heap_dump") + if got == nil { + t.Errorf("This should produce an Error") + } + if !reflect.DeepEqual(got.Error(), want.Error()) { + t.Errorf("got %+v, want %+v", got, want) + } +} + +func TestHandleNewHeapDump(t *testing.T) { + config := cfg.AppConfig{ + Metrics: struct { + Port int + Path string + }{ + Port: 8081, + Path: "/metrics", + }, + WatchPath: struct{ Path string }{ + Path: "/test", + }, + Middleware: struct{ Endpoint string }{ + Endpoint: "http://localhost:21347/request/good", + }, + ServiceOwner: struct { + Tenant string + }{ + Tenant: "testTenant", + }, + } + fs := fstest.MapFS{ + "var/run/secrets/kubernetes.io/serviceaccount/token": {Data: []byte("test_token")}, + "var/run/secrets/kubernetes.io/serviceaccount/namespace": {Data: []byte("platform")}, + "test_heap_dump": {Data: []byte("dummy")}, + } + err := handleNewHeapDump(fs, config, "test_heap_dump") + if err == nil { + t.Errorf("This should fail!") + } + if !(strings.Contains(err.Error(), "Error making request")) { + t.Errorf("Got error: %s , want: %s", err.Error(), "Error making request: Put \"http://test-url.org/upload\": dial tcp: lookup test-url.org on 10.227.160.2:53: no such host") + } + cleanup("test_heap_dump.crypted") +} diff --git a/notify-sidecar/config/docker-config.json b/notify-sidecar/config/docker-config.json new file mode 100644 index 0000000..5c135b1 --- /dev/null +++ b/notify-sidecar/config/docker-config.json @@ -0,0 +1,15 @@ +{ + "metrics": { + "port": 8081, + "path": "/metrics" + }, + "WatchPath": { + "path": "/heap-dumps/" + }, + "Middleware": { + "endpoint": "https://centraluploadservice.srv.cluster.local" + }, + "ServiceOwner": { + "tenant": "devops" + } +} \ No newline at end of file diff --git a/notify-sidecar/config/local-config.json b/notify-sidecar/config/local-config.json new file mode 100644 index 0000000..2d8774a --- /dev/null +++ b/notify-sidecar/config/local-config.json @@ -0,0 +1,15 @@ +{ + "metrics": { + "port": 8081, + "path": "/metrics" + }, + "WatchPath": { + "path": "/mnt/dumps" + }, + "Middleware": { + "endpoint": "https://centraluploadservice.srv.cluster.local" + }, + "ServiceOwner": { + "tenant": "devops" + } +} \ No newline at end of file diff --git a/notify-sidecar/config/test/bad-config.json b/notify-sidecar/config/test/bad-config.json new file mode 100644 index 0000000..33c0e97 --- /dev/null +++ b/notify-sidecar/config/test/bad-config.json @@ -0,0 +1,5 @@ +{ + "invalid": { + "data": "here" + } +} \ No newline at end of file diff --git a/notify-sidecar/config/test/test-config.json b/notify-sidecar/config/test/test-config.json new file mode 100644 index 0000000..bdf9fd8 --- /dev/null +++ b/notify-sidecar/config/test/test-config.json @@ -0,0 +1,15 @@ +{ + "metrics": { + "port": 8081, + "path": "/metrics" + }, + "WatchPath": { + "path": "/test" + }, + "Middleware": { + "endpoint": "https://test.svc.cluster.local" + }, + "ServiceOwner": { + "tenant": "testTenant" + } +} \ No newline at end of file diff --git a/notify-sidecar/docs/Architecture.svg b/notify-sidecar/docs/Architecture.svg new file mode 100644 index 0000000..7670ba7 --- /dev/null +++ b/notify-sidecar/docs/Architecture.svg @@ -0,0 +1 @@ +ApplicationApplicationNotifySidecarNotifySidecarHeapDumpServiceHeapDumpServiceVaultVaultAWSAWSwrites heap dump in shared volumeRequest Upload URLEncrypt AES Key with tenant keyGenerate PreSigned upload URL to S3Returns Key, Encrypted Key and URLEncrypts Heap DumpUploads encrypted heap dump and encrypted AES Key to S3Terminates \ No newline at end of file diff --git a/notify-sidecar/docs/README.md b/notify-sidecar/docs/README.md new file mode 100644 index 0000000..a834f18 --- /dev/null +++ b/notify-sidecar/docs/README.md @@ -0,0 +1,5 @@ +# Example + +## backend + +This is example backend diff --git a/notify-sidecar/docs/architecture.plantuml b/notify-sidecar/docs/architecture.plantuml new file mode 100644 index 0000000..e9728fe --- /dev/null +++ b/notify-sidecar/docs/architecture.plantuml @@ -0,0 +1,12 @@ +@startuml Architecture + +Application -> NotifySidecar: writes heap dump in shared volume +NotifySidecar -> HeapDumpService: Request Upload URL +HeapDumpService -> Vault: Encrypt AES Key with tenant key +HeapDumpService -> AWS: Generate PreSigned upload URL to S3 +HeapDumpService -> NotifySidecar: Returns Key, Encrypted Key and URL +NotifySidecar -> NotifySidecar: Encrypts Heap Dump +NotifySidecar -> AWS: Uploads encrypted heap dump and encrypted AES Key to S3 +Application -> Application: Terminates + +@enduml \ No newline at end of file diff --git a/notify-sidecar/docs/config.md b/notify-sidecar/docs/config.md new file mode 100644 index 0000000..dd5042e --- /dev/null +++ b/notify-sidecar/docs/config.md @@ -0,0 +1,97 @@ +# Setup + +In order to make use of the notify sidecar some preparations need to be taken. Primarily in order to configure the location of the shared volume for a java application to write information to and the location of the central heap dump service. + +## Kubernetes + +As the name suggest the notify sidecar should be ran as a sidecar next to the actual application. This deployment will give a minimal example how it should look like. Please take note on the shared volume and the java opts to ensure the jvm will write to the correct location in case a OOMException is thrown. + +```yaml +apiVersion: apps/v1 +kind: Deployment +spec: + selector: + matchLabels: + app.kubernetes.io/instance: java-app + app.kubernetes.io/name: java-app + template: + spec: + containers: + - name: java-app + image: example-java-application + env: + - name: JAVA_TOOL_OPTIONS + value: -Xms512m -Xmx512m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/heap-dumps/ + resources: + limits: + memory: 1Gi # needs to be higher than maximum heap + requests: + memory: 1Gi + volumeMounts: + - mountPath: /heap-dumps + name: heap-dumps + - name: notify-sidecar + image: ghcr.io/dbschenker/heap-dump-management/notify-sidecar:release-1.1.3-28cc0112 + command: + - /app + env: + - name: APP_CONFIG_FILE + value: /opt/config.json + - name: POD_NAME + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: metadata.name + - name: NOTIFY_SIDECAR_LOG_LEVEL + value: WARNING + volumeMounts: + - mountPath: /heap-dumps + name: heap-dumps + - mountPath: /opt + name: heap-dump-config + volumes: + - emptyDir: {} + name: heap-dumps + - configMap: + defaultMode: 420 + name: java-app-heap-dump-cm + name: heap-dump-config +``` + +# Configuration + +The heap dump service can be configured with a json config and environment variables. +Here is an example json configuration: + +```json +{ + "metrics": { + "port": 8081, + "path": "/metrics" + }, + "WatchPath": { + "path": "/heap-dumps" + }, + "Middleware": { + "endpoint": "https://test.svc.cluster.local" + }, + "ServiceOwner": { + "tenant": "testTenant" + } +} +``` + +this `config.json` file can be referenced by the environment variable `APP_CONFIG_JSON`. +Other environment variables include: + +```yaml +- name: APP_CONFIG_FILE + value: /opt/config.json +- name: POD_NAME + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: metadata.name +- name: NOTIFY_SIDECAR_LOG_LEVEL + value: WARNING +``` diff --git a/notify-sidecar/docs/diagram.plantuml b/notify-sidecar/docs/diagram.plantuml new file mode 100644 index 0000000..68d52f1 --- /dev/null +++ b/notify-sidecar/docs/diagram.plantuml @@ -0,0 +1,25 @@ +@startuml +title Notify Sidecar + +start + +:Serve Metrics; +while (Enter main loop) +if (File being written) then (yes) + :Check file size; + if (File bigger than 16 MB) then (yes) + :wait until write operation is finished; + :Request presigned url and AES key; + :Encrypt Heap Dump; + :Upload encrypted data to S3 bucket; + :Cleanup data; + else (no) + :remove file; + endif +else (no) + :Wait; +endif +endwhile (shutdown) + +stop +@enduml \ No newline at end of file diff --git a/notify-sidecar/docs/diagram.svg b/notify-sidecar/docs/diagram.svg new file mode 100644 index 0000000..15814fa --- /dev/null +++ b/notify-sidecar/docs/diagram.svg @@ -0,0 +1 @@ +Notify SidecarNotify SidecarServe MetricsFile being writtenyesnoCheck file sizeFile bigger than 16 MByesnowait until write operation is finishedRequest presigned url and AES keyEncrypt Heap DumpUpload encrypted data to S3 bucketCleanup dataremove fileWaitEnter main loopshutdown \ No newline at end of file diff --git a/notify-sidecar/go.mod b/notify-sidecar/go.mod new file mode 100644 index 0000000..cd1b5db --- /dev/null +++ b/notify-sidecar/go.mod @@ -0,0 +1,25 @@ +module github.com/dbschenker/heap-dump-management/notify-sidecar + +go 1.22 + +require ( + github.com/prometheus/client_golang v1.20.5 + github.com/sirupsen/logrus v1.9.3 + github.com/stretchr/testify v1.10.0 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/klauspost/compress v1.17.11 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.61.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + golang.org/x/sys v0.28.0 // indirect + google.golang.org/protobuf v1.35.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/notify-sidecar/go.sum b/notify-sidecar/go.sum new file mode 100644 index 0000000..e079738 --- /dev/null +++ b/notify-sidecar/go.sum @@ -0,0 +1,49 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +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/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/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/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_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= +github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.61.0 h1:3gv/GThfX0cV2lpO7gkTUwZru38mxevy90Bj8YFSRQQ= +github.com/prometheus/common v0.61.0/go.mod h1:zr29OCN/2BsJRaFwG8QOBr41D6kkchKbpeNH7pAjb/s= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= +google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/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= diff --git a/notify-sidecar/internal/config/config.go b/notify-sidecar/internal/config/config.go new file mode 100644 index 0000000..54b2670 --- /dev/null +++ b/notify-sidecar/internal/config/config.go @@ -0,0 +1,61 @@ +package config + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "strings" + + log "github.com/sirupsen/logrus" +) + +type AppConfig struct { + Metrics struct { + Port int + Path string + } + WatchPath struct { + Path string + } + Middleware struct { + Endpoint string + } + ServiceOwner struct { + Tenant string + } +} + +func LoadConfigFromEnvironment(envVarName string) (AppConfig, error) { + configFile, found := os.LookupEnv(envVarName) + var appConfig AppConfig + if !found { + log.WithFields(log.Fields{ + "caller": "LoadConfigFromEnvironment", + }).Warnf(fmt.Sprintf("Environment variable for config file not set: %s", envVarName)) + return appConfig, errors.New(fmt.Sprintf("Environment variable for config file not set: %s", envVarName)) + } + return LoadConfigFromFile(configFile) +} + +func LoadConfigFromFile(configFile string) (AppConfig, error) { + jsonData, err := os.ReadFile(configFile) + var appConfig AppConfig + if err != nil { + log.WithFields(log.Fields{ + "caller": "LoadConfigFromEnvironment", + }).Warnf(fmt.Sprintf("Failed to load config file '%v': %v", configFile, err)) + return appConfig, errors.New(fmt.Sprintf("Failed to load config file '%v': %v", configFile, err.Error())) + } + + d := json.NewDecoder(strings.NewReader(string(jsonData))) + d.DisallowUnknownFields() + err = d.Decode(&appConfig) + if err != nil { + log.WithFields(log.Fields{ + "caller": "LoadConfigFromEnvironment", + }).Warnf(fmt.Sprintf("Failed to parse json data of file '%v': %v", configFile, err)) + return appConfig, errors.New(fmt.Sprintf("Failed to parse json data of file '%v': %v", configFile, err.Error())) + } + return appConfig, nil +} diff --git a/notify-sidecar/internal/config/config_test.go b/notify-sidecar/internal/config/config_test.go new file mode 100644 index 0000000..8e198f7 --- /dev/null +++ b/notify-sidecar/internal/config/config_test.go @@ -0,0 +1,95 @@ +package config + +import ( + "errors" + "fmt" + "os" + "reflect" + "testing" +) + +var Want = AppConfig{ + Metrics: struct { + Port int + Path string + }{ + Port: 8081, + Path: "/metrics", + }, + WatchPath: struct{ Path string }{ + Path: "/test", + }, + Middleware: struct{ Endpoint string }{ + Endpoint: "https://test.svc.cluster.local", + }, + ServiceOwner: struct { + Tenant string + }{ + Tenant: "testTenant", + }, +} + +func TestLoadConfigFromFile(t *testing.T) { + got, err := LoadConfigFromFile("../../config/test/test-config.json") + if err != nil { + t.Errorf("Failed to construct config: %v", err) + } + if !reflect.DeepEqual(got, Want) { + t.Errorf("got %+v, want %+v", got, Want) + } +} + +func TestFailLoadConfigFromFile(t *testing.T) { + want := errors.New(fmt.Sprintf("Failed to load config file '%v': %v", "does-not-exist.json", "open does-not-exist.json: no such file or directory")) + _, got := LoadConfigFromFile("does-not-exist.json") + if got == nil { + t.Errorf("This should produce an error!") + } + if !reflect.DeepEqual(got.Error(), want.Error()) { + t.Errorf("got %+v, want %+v", got, want) + } + want = errors.New(fmt.Sprintf("Failed to parse json data of file '%v': %v", "../../config/test/bad-config.json", "json: unknown field \"invalid\"")) + _, got = LoadConfigFromFile("../../config/test/bad-config.json") + if got == nil { + t.Errorf("This should produce an error!") + } + if !reflect.DeepEqual(got.Error(), want.Error()) { + t.Errorf("got %+v, want %+v", got, want) + } +} + +func TestLoadConfigFromEnv(t *testing.T) { + os.Setenv("TEST_APP_CONFIG_FILE", "../../config/test/test-config.json") + defer os.Unsetenv("TEST_APP_CONFIG_FILE") + got, err := LoadConfigFromEnvironment("TEST_APP_CONFIG_FILE") + if err != nil { + t.Errorf("Failed to construct config: %v", err) + } + if !reflect.DeepEqual(got, Want) { + t.Errorf("got %+v, want %+v", got, Want) + } +} + +func TestFailLoadConfigFromEnvironment(t *testing.T) { + want := errors.New(fmt.Sprintf("Failed to load config file '%v': %v", "does-not-exist.json", "open does-not-exist.json: no such file or directory")) + os.Setenv("TEST_APP_CONFIG_FILE", "does-not-exist.json") + defer os.Unsetenv("TEST_APP_CONFIG_FILE") + _, got := LoadConfigFromEnvironment("TEST_APP_CONFIG_FILE") + if got == nil { + t.Errorf("This should produce an error!") + } + if !reflect.DeepEqual(got.Error(), want.Error()) { + t.Errorf("got %+v, want %+v", got, want) + } +} + +func TestFailNoConfigEnv(t *testing.T) { + want := errors.New(fmt.Sprintf("Environment variable for config file not set: %s", "TEST_NO_ENV")) + _, got := LoadConfigFromEnvironment("TEST_NO_ENV") + if got == nil { + t.Errorf("This should produce an error!") + } + if !reflect.DeepEqual(got.Error(), want.Error()) { + t.Errorf("got %+v, want %+v", got, want) + } +} diff --git a/notify-sidecar/internal/logging/logging.go b/notify-sidecar/internal/logging/logging.go new file mode 100644 index 0000000..c94460d --- /dev/null +++ b/notify-sidecar/internal/logging/logging.go @@ -0,0 +1,25 @@ +package logging + +import ( + "fmt" + "os" + + log "github.com/sirupsen/logrus" +) + +func SetupLogging() { + logLevelString, found := os.LookupEnv("NOTIFY_SIDECAR_LOG_LEVEL") + if !found { + logLevelString = "WARNING" + } + level, err := log.ParseLevel(logLevelString) + if err != nil { + log.WithFields(log.Fields{ + "caller": "SetupLogging", + }).Error(fmt.Sprintf("error parsing %s: %v", logLevelString, err)) + panic(err) + } + log.SetFormatter(&log.JSONFormatter{}) + log.SetOutput(os.Stdout) + log.SetLevel(level) +} diff --git a/notify-sidecar/internal/logging/logging_test.go b/notify-sidecar/internal/logging/logging_test.go new file mode 100644 index 0000000..2e4f338 --- /dev/null +++ b/notify-sidecar/internal/logging/logging_test.go @@ -0,0 +1,39 @@ +package logging + +import ( + "os" + "reflect" + "testing" + + log "github.com/sirupsen/logrus" +) + +func TestSetupLogging(t *testing.T) { + SetupLogging() + if log.GetLevel() != log.WarnLevel { + t.Errorf("got %+v, want %+v", log.GetLevel(), log.WarnLevel) + } + if !reflect.DeepEqual(log.StandardLogger().Formatter, &log.JSONFormatter{}) { + t.Errorf("got %+v, want %+v", log.StandardLogger().Formatter, &log.JSONFormatter{}) + } + if !reflect.DeepEqual(log.StandardLogger().Out, os.Stdout) { + t.Errorf("got %+v, want %+v", log.StandardLogger().Out, os.Stdout) + } +} + +func TestSetupLoggingWithEnv(t *testing.T) { + os.Setenv("NOTIFY_SIDECAR_LOG_LEVEL", "INFO") + defer os.Unsetenv("NOTIFY_SIDECAR_LOG_LEVEL") + SetupLogging() + if log.GetLevel() != log.InfoLevel { + t.Errorf("got %+v, want %+v", log.GetLevel(), log.InfoLevel) + } +} + +func TestSetupLoggingWithBadEnv(t *testing.T) { + os.Setenv("NOTIFY_SIDECAR_LOG_LEVEL", "Biggus Dickus") + defer os.Unsetenv("NOTIFY_SIDECAR_LOG_LEVEL") + defer func() { _ = recover() }() + SetupLogging() + t.Errorf("Setting and invalid log level should result in a panic") +} diff --git a/notify-sidecar/internal/metrics/metrics.go b/notify-sidecar/internal/metrics/metrics.go new file mode 100644 index 0000000..a7e686b --- /dev/null +++ b/notify-sidecar/internal/metrics/metrics.go @@ -0,0 +1,43 @@ +package metrics + +import ( + "fmt" + "net/http" + + log "github.com/sirupsen/logrus" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +var HeapDumpHandled = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "handled_heap_dumps", + Namespace: "heap_dump_service", + Help: "Number of handled heap dumps", + }, + []string{"tenant"}, +) + +var FailedDumps = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "failed_heap_dumps", + Namespace: "heap_dump_service", + Help: "Number of failed heap dumps", + }, + []string{"tenant"}, +) + +func init() { + prometheus.MustRegister(HeapDumpHandled) + prometheus.MustRegister(FailedDumps) +} + +func StartMetricServer(port int, path string) { + http.Handle(path, promhttp.Handler()) + hostAddress := fmt.Sprintf(":%v", port) + log.WithFields(log.Fields{ + "caller": "StartMetricServer", + }).Info("Serving Metrics") + http.ListenAndServe(hostAddress, nil) +} diff --git a/notify-sidecar/internal/metrics/metrics_test.go b/notify-sidecar/internal/metrics/metrics_test.go new file mode 100644 index 0000000..06ffbd7 --- /dev/null +++ b/notify-sidecar/internal/metrics/metrics_test.go @@ -0,0 +1,54 @@ +package metrics + +import ( + "net/http" + "strings" + "testing" + "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/stretchr/testify/assert" +) + +func TestMetricsInitialization(t *testing.T) { + // Ensure that the metrics are registered + HeapDumpHandled.WithLabelValues("test_tenant").Inc() + FailedDumps.WithLabelValues("test_tenant").Inc() + + metricFamilies, err := prometheus.DefaultGatherer.Gather() + assert.NoError(t, err) + + var foundHeapDumpHandled, foundFailedDumps bool + for _, mf := range metricFamilies { + if mf.GetName() == "heap_dump_service_handled_heap_dumps" { + foundHeapDumpHandled = true + } + if mf.GetName() == "heap_dump_service_failed_heap_dumps" { + foundFailedDumps = true + } + } + + assert.True(t, foundHeapDumpHandled, "handled_heap_dumps metric not found") + assert.True(t, foundFailedDumps, "failed_heap_dumps metric not found") +} + +func TestPrometheusServerStartup(t *testing.T) { + + go func() { + StartMetricServer(21338, "/metrics") + }() + // Metrics Server needs a few seconds to start up + time.Sleep(2 * time.Second) + + request, _ := http.NewRequest(http.MethodGet, "http://localhost:21338/metrics", strings.NewReader("")) + resp, err := http.DefaultClient.Do(request) + + if err != nil { + t.Errorf("Metrics endpoint did not start: %v", err) + } + + if resp.StatusCode != http.StatusOK { + t.Errorf("Want status '%d', got '%d'", http.StatusOK, resp.StatusCode) + } + +} diff --git a/notify-sidecar/internal/models/structs.go b/notify-sidecar/internal/models/structs.go new file mode 100644 index 0000000..7504e50 --- /dev/null +++ b/notify-sidecar/internal/models/structs.go @@ -0,0 +1,14 @@ +package models + +type Payload struct { + Tenant string `json:"tenant"` + Namespace string `json:"namespace"` + FileName string `json:"filename"` +} + +type SigningResponse struct { + URL string `json:"url"` + EncryptedAesKey string `json:"encrypted-aes-key"` + EncryptedAesKeyURL string `json:"encrypted-aes-key-url"` + AesKey string `json:"aes-key"` +} diff --git a/notify-sidecar/internal/utils/coms.go b/notify-sidecar/internal/utils/coms.go new file mode 100644 index 0000000..7be7c43 --- /dev/null +++ b/notify-sidecar/internal/utils/coms.go @@ -0,0 +1,81 @@ +package utils + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "io/fs" + "net/http" + "os" + "path/filepath" + "time" + + "github.com/dbschenker/heap-dump-management/notify-sidecar/internal/config" + "github.com/dbschenker/heap-dump-management/notify-sidecar/internal/models" +) + +func constructBearerAuth(fileSystem fs.FS, tokenLocation string) (string, error) { + sAToken, err := fs.ReadFile(fileSystem, tokenLocation) + if err != nil { + return "", errors.New(fmt.Sprintf("Error reading SA Token: %s", err.Error())) + } + return fmt.Sprintf("Bearer %s", string(sAToken)), nil +} + +func constructRequestBody(fileName string, tenant string, namespace string, podName string) (*bytes.Reader, error) { + t := time.Now() + data := models.Payload{ + Tenant: tenant, + Namespace: namespace, + FileName: fmt.Sprintf("%s-%s-%s.hprof.crypted", podName, fileName, t.Format("2006-01-02-15-04-05")), + } + + payloadBytes, err := json.Marshal(data) + if err != nil { + return nil, errors.New(fmt.Sprintf("Error creating middleware request body: %s", err.Error())) + } + return bytes.NewReader(payloadBytes), nil + +} + +func RequestUploadConfig(fileSystem fs.FS, cfg config.AppConfig, fileName string, target interface{}) error { + + bearer, err := constructBearerAuth(fileSystem, "var/run/secrets/kubernetes.io/serviceaccount/token") + if err != nil { + return err + } + + ns, err := GetCurrentNamespace(fileSystem) + + CheckError(err) + + podName := os.Getenv("POD_NAME") + + body, err := constructRequestBody(filepath.Base(fileName), cfg.ServiceOwner.Tenant, ns, podName) + if err != nil { + return err + } + + req, err := http.NewRequest("POST", cfg.Middleware.Endpoint, body) + req.Header.Add("Authorization", bearer) + if err != nil { + return errors.New(fmt.Sprintf("Error creating request to middleware: %s", err.Error())) + } + req.Header.Set("Content-Type", "application/json") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return errors.New(fmt.Sprintf("Error sending request to middleware: %s", err.Error())) + } + + defer resp.Body.Close() + + if resp.StatusCode != 200 { + b, _ := io.ReadAll(resp.Body) + return errors.New(fmt.Sprintf("Middleware replied with error code: %d: %s", resp.StatusCode, b)) + } + + return json.NewDecoder(resp.Body).Decode(target) +} diff --git a/notify-sidecar/internal/utils/coms_test.go b/notify-sidecar/internal/utils/coms_test.go new file mode 100644 index 0000000..97ffffb --- /dev/null +++ b/notify-sidecar/internal/utils/coms_test.go @@ -0,0 +1,184 @@ +package utils + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "log" + "net/http" + "os" + "reflect" + "strings" + "testing" + "testing/fstest" + "time" + + "github.com/dbschenker/heap-dump-management/notify-sidecar/internal/config" + "github.com/dbschenker/heap-dump-management/notify-sidecar/internal/models" +) + +var TestResponse = models.SigningResponse{ + URL: "http://test-url.org/upload", + EncryptedAesKey: "cryptedTest", + EncryptedAesKeyURL: "http://test-url.org/upload2", + AesKey: "test", +} + +var ValidFs = fstest.MapFS{ + "var/run/secrets/kubernetes.io/serviceaccount/token": {Data: []byte("test_token")}, + "var/run/secrets/kubernetes.io/serviceaccount/namespace": {Data: []byte("platform")}, +} + +var InvalidFs = fstest.MapFS{ + "invalid": {Data: []byte("none")}, +} + +func TestMain(m *testing.M) { + httpServer := http.Server{ + Addr: ":21337", + } + setup(&httpServer) + time.Sleep(1000 * time.Millisecond) + m.Run() + shutdown(&httpServer) +} + +func setup(serverPointer *http.Server) { + returnGoodJson, _ := json.Marshal(TestResponse) + http.HandleFunc("/request", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, string(returnGoodJson)) + }) + go func() { + if err := serverPointer.ListenAndServe(); err != http.ErrServerClosed { + log.Fatalf("HTTP server ListenAndServe Error: %v", err) + } + }() +} + +func shutdown(serverPointer *http.Server) { + if err := serverPointer.Shutdown(context.Background()); err != nil { + log.Printf("HTTP Server Shutdown Error: %v", err) + } +} + +func TestConstructBearerAuth(t *testing.T) { + want := "Bearer test_token" + got, err := constructBearerAuth(ValidFs, "var/run/secrets/kubernetes.io/serviceaccount/token") + if err != nil { + t.Errorf("Failed to construct authHeader: %v", err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("got %+v, want %+v", got, want) + } +} + +func TestConstructRequestBody(t *testing.T) { + testSystem := "devops" + testComponent := "platform" + testFileName := "heapDump" + testPodName := "TestingPod" + now := time.Now() + + os.Setenv("POD_NAME", testPodName) + + testData := models.Payload{ + Tenant: testSystem, + Namespace: testComponent, + FileName: fmt.Sprintf("%s-%s-%s.hprof.crypted", testPodName, testFileName, now.Format("2006-01-02-15-04-05")), + } + + testBytes, _ := json.Marshal(testData) + want := bytes.NewReader(testBytes) + got, err := constructRequestBody(testFileName, testSystem, testComponent, testPodName) + if err != nil { + t.Errorf("Failed to construct request body: %v", err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("got %+v, want %+v", got, want) + } +} + +func TestRequestUploadConfig(t *testing.T) { + testConfig := config.AppConfig{ + Metrics: struct { + Port int + Path string + }{ + Port: 8081, + Path: "/metrics", + }, + WatchPath: struct{ Path string }{ + Path: "/test", + }, + Middleware: struct{ Endpoint string }{ + Endpoint: "http://localhost:21337/request", + }, + ServiceOwner: struct { + Tenant string + }{ + Tenant: "testTenant", + }, + } + + got := new(models.SigningResponse) + + /*go func() { + mockMiddleware() + }()*/ + + err := RequestUploadConfig(ValidFs, testConfig, "test", got) + + if err != nil { + t.Errorf("Error requesting upload config %v", err) + } + if !reflect.DeepEqual(*got, TestResponse) { + t.Errorf("got %+v, want %+v", got, TestResponse) + } +} + +func TestBadRequestUploadConfig(t *testing.T) { + badTestConfig := config.AppConfig{ + Metrics: struct { + Port int + Path string + }{ + Port: 8081, + Path: "/metrics", + }, + WatchPath: struct{ Path string }{ + Path: "/test", + }, + Middleware: struct{ Endpoint string }{ + Endpoint: "http://localhost:1337/request", + }, + ServiceOwner: struct { + Tenant string + }{ + Tenant: "testTenant", + }, + } + testResponseModel := new(models.SigningResponse) + + got := RequestUploadConfig(InvalidFs, badTestConfig, "does not matter", testResponseModel) + wantNoToken := errors.New(fmt.Sprintf("Error reading SA Token: %s", "open var/run/secrets/kubernetes.io/serviceaccount/token: file does not exist")) + + if got == nil { + t.Errorf("Request should not be constructed without a valid token!") + } + if !reflect.DeepEqual(got.Error(), wantNoToken.Error()) { + t.Errorf("got %+v, want %+v", got.Error(), wantNoToken.Error()) + } + + got = RequestUploadConfig(ValidFs, badTestConfig, "does not matter", testResponseModel) + wantNoNetwork := "Error sending request to middleware" + + if got == nil { + t.Errorf("Network Failure should probagate error!") + } + if !(strings.Contains(got.Error(), wantNoNetwork)) { + t.Errorf("got wrong error %+v, want %+v", got.Error(), wantNoNetwork) + } + +} diff --git a/notify-sidecar/internal/utils/encryption.go b/notify-sidecar/internal/utils/encryption.go new file mode 100644 index 0000000..457327b --- /dev/null +++ b/notify-sidecar/internal/utils/encryption.go @@ -0,0 +1,64 @@ +package utils + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "errors" + "fmt" + "io" + "io/fs" + "os" + "path/filepath" +) + +const chunkSize = 64 * 1024 // 64 KB + +func EncryptDump(fileSystem fs.FS, fileLocation string, key []byte) (string, error) { + // Reading plaintext file + inputFile, err := fileSystem.Open(fileLocation) + if err != nil { + return "", errors.New(fmt.Sprintf("Error reading heap dump %s: %s", fileLocation, err.Error())) + } + + // Creating block of algorithm + block, err := aes.NewCipher(key) + if err != nil { + return "", errors.New(fmt.Sprintf("Error initializing ARE Cipher: %s", err.Error())) + } + + // Creating GCM mode + gcm, err := cipher.NewGCM(block) + if err != nil { + return "", errors.New(fmt.Sprintf("Error in GCM Cipher: %s", err.Error())) + } + + // Generating random nonce + nonce := make([]byte, gcm.NonceSize()) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + return "", errors.New(fmt.Sprintf("Error generating random nonce: %s", err.Error())) + } + + buffer := make([]byte, chunkSize) + outputFile, err := os.Create(fmt.Sprintf("/tmp/%s.%s", filepath.Base(fileLocation), "crypted")) + if err != nil { + return "", errors.New(fmt.Sprintf("Error creating encrypted heap dump: %s", err.Error())) + } + + for { + n, err := inputFile.Read(buffer) + if err != nil && err != io.EOF { + return "", errors.New(fmt.Sprintf("Error reading heap dump: %s", err.Error())) + } + if n == 0 { + break + } + + encryptedChunk := gcm.Seal(nil, nonce, buffer[:n], nil) + if _, err := outputFile.Write(encryptedChunk); err != nil { + return "", errors.New(fmt.Sprintf("Error writing encrypted heap dump: %s", err.Error())) + } + } + + return fmt.Sprintf("/tmp/%s.%s", filepath.Base(fileLocation), "crypted"), nil +} diff --git a/notify-sidecar/internal/utils/encryption_test.go b/notify-sidecar/internal/utils/encryption_test.go new file mode 100644 index 0000000..bf182fd --- /dev/null +++ b/notify-sidecar/internal/utils/encryption_test.go @@ -0,0 +1,72 @@ +package utils + +import ( + "crypto/rand" + "errors" + "fmt" + "os" + "reflect" + "testing" + "testing/fstest" +) + +func cleanup(target string) { + os.Remove(target) +} + +func TestEncryption(t *testing.T) { + fs := fstest.MapFS{ + "test_heap_dump": {Data: []byte("asdfasdfasdf")}, + } + test_key := []byte{52, 74, 93, 7, 97, 74, 50, 186, 172, 14, 125, 208, 130, 218, 177, 215, 219, 219, 247, 163, 81, 86, 105, 60, 22, 162, 54, 81, 19, 37, 212, 49} + want := "/tmp/test_heap_dump.crypted" + got, err := EncryptDump(fs, "test_heap_dump", test_key) + + if err != nil { + t.Errorf("Failed to encrypt test file: %v", err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("got %+v, want %+v", got, want) + } + _, err = os.Stat(want) + if err != nil { + t.Errorf("Encrypted test file does not exist: %v", err) + } + cleanup(want) +} + +func TestBadEncryption(t *testing.T) { + fs := fstest.MapFS{ + "test_heap_dump": {Data: []byte("asdfasdfasdf")}, + } + test_key := make([]byte, 8) + rand.Read(test_key) + + _, got := EncryptDump(fs, "test_heap_dump", test_key) + want := errors.New(fmt.Sprintf("Error initializing ARE Cipher: %s", "crypto/aes: invalid key size 8")) + + if got == nil { + t.Errorf("AES should not be used with a key size of 8") + } + if !reflect.DeepEqual(got.Error(), want.Error()) { + t.Errorf("got %+v, want %+v", got, want) + } +} + +func TestFileNotFound(t *testing.T) { + fs := fstest.MapFS{ + "test_heap_dump": {Data: []byte("asdfasdfasdf")}, + } + test_key := make([]byte, 8) + rand.Read(test_key) + + _, got := EncryptDump(fs, "invalid", test_key) + want := errors.New(fmt.Sprintf("Error reading heap dump %s: %s", "invalid", "open invalid: file does not exist")) + + if got == nil { + t.Errorf("File should not exist") + } + if !reflect.DeepEqual(got.Error(), want.Error()) { + t.Errorf("got %+v, want %+v", got, want) + } +} diff --git a/notify-sidecar/internal/utils/general.go b/notify-sidecar/internal/utils/general.go new file mode 100644 index 0000000..8f29dda --- /dev/null +++ b/notify-sidecar/internal/utils/general.go @@ -0,0 +1,25 @@ +package utils + +import ( + "errors" + "fmt" + "io/fs" + + log "github.com/sirupsen/logrus" +) + +func CheckError(err error) { + if err != nil { + log.WithFields(log.Fields{ + "caller": "CheckError", + }).Fatalf(err.Error()) + } +} + +func GetCurrentNamespace(fileSystem fs.FS) (string, error) { + namespace, err := fs.ReadFile(fileSystem, "var/run/secrets/kubernetes.io/serviceaccount/namespace") + if err != nil { + return "", errors.New(fmt.Sprintf("Could not get Namespace: %s", err.Error())) + } + return string(namespace), nil +} diff --git a/notify-sidecar/internal/utils/general_test.go b/notify-sidecar/internal/utils/general_test.go new file mode 100644 index 0000000..8592c7c --- /dev/null +++ b/notify-sidecar/internal/utils/general_test.go @@ -0,0 +1,38 @@ +package utils + +import ( + "reflect" + "strings" + "testing" + "testing/fstest" +) + +func TestCheckError(*testing.T) { + CheckError(nil) +} + +func TestNamespace(t *testing.T) { + fs := fstest.MapFS{ + "var/run/secrets/kubernetes.io/serviceaccount/namespace": {Data: []byte("testNamespace")}, + } + want := "testNamespace" + wantError := "Could not get Namespace" + got, err := GetCurrentNamespace(fs) + + if err != nil { + t.Errorf("Failed to encrypt test file: %v", err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("got %+v, want %+v", got, want) + } + fs = fstest.MapFS{ + "NotInK8s": {Data: []byte("testNamespace")}, + } + got, err = GetCurrentNamespace(fs) + if err == nil { + t.Errorf("This should Fail") + } + if !(strings.Contains(err.Error(), wantError)) { + t.Errorf("got %+v, want %+v", err.Error(), wantError) + } +} diff --git a/notify-sidecar/internal/utils/upload.go b/notify-sidecar/internal/utils/upload.go new file mode 100644 index 0000000..f3d59ed --- /dev/null +++ b/notify-sidecar/internal/utils/upload.go @@ -0,0 +1,30 @@ +package utils + +import ( + "bytes" + "errors" + "fmt" + "io" + "net/http" +) + +func UploadToS3(url string, file io.Reader) error { + + buf := &bytes.Buffer{} + buf.ReadFrom(file) + + req, err := http.NewRequest("PUT", url, buf) + if err != nil { + return errors.New(fmt.Sprintf("Error creating request %s: %s", url, err.Error())) + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return errors.New(fmt.Sprintf("Error making request: %s", err.Error())) + } + b, _ := io.ReadAll(resp.Body) + if resp.StatusCode > 400 { + return errors.New(fmt.Sprintf("AWS Api responded with status %d : %s", resp.StatusCode, b)) + } + return nil +} diff --git a/notify-sidecar/internal/utils/upload_test.go b/notify-sidecar/internal/utils/upload_test.go new file mode 100644 index 0000000..4e54f69 --- /dev/null +++ b/notify-sidecar/internal/utils/upload_test.go @@ -0,0 +1,17 @@ +package utils + +import ( + "os" + "strings" + "testing" +) + +func TestFailedUpload(t *testing.T) { + fileHandler, _ := os.Open("does_not_exist") + url := "http://localhost:1337" + got := UploadToS3(url, fileHandler) + wantNoNetwork := "Error making request:" + if !(strings.Contains(got.Error(), wantNoNetwork)) { + t.Errorf("got wrong error %+v, want %+v", got.Error(), wantNoNetwork) + } +} diff --git a/notify-sidecar/sonar-project.properties b/notify-sidecar/sonar-project.properties new file mode 100644 index 0000000..2a59890 --- /dev/null +++ b/notify-sidecar/sonar-project.properties @@ -0,0 +1,11 @@ +sonar.projectKey=com.dbschenker.gilds.devops.heap-dump-management_notify-sidecar +sonar.projectName=Heap Dump Notify Sidecar + +sonar.sources=. +sonar.exclusions=**/*_test.go,**/main.go,.cache/** + +sonar.tests=. +sonar.test.inclusions=**/*_test.go + +sonar.go.coverage.reportPaths=coverage.out +sonar.go.tests.reportPaths=report.json \ No newline at end of file