Compare commits

..

No commits in common. "main" and "v1.3.0" have entirely different histories.
main ... v1.3.0

72 changed files with 339 additions and 4477 deletions

View File

@ -1,10 +0,0 @@
.idea/
vendor/
dist/
dump/
dumpcerts.sh
acme.json
acme-backup.json
traefik-certs-dumper
manifest.json
*.Dockerfile

4
.github/FUNDING.yml vendored
View File

@ -1,4 +0,0 @@
github: ldez
ko_fi: ldez_oss
liberapay: ldez
thanks_dev: u/gh/ldez

View File

@ -1,40 +0,0 @@
name: Go Matrix
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
cross:
name: Go
runs-on: ${{ matrix.os }}
env:
CGO_ENABLED: 0
strategy:
matrix:
go-version: [ stable ]
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
# https://github.com/marketplace/actions/checkout
- name: Checkout code
uses: actions/checkout@v4
# https://github.com/marketplace/actions/setup-go-environment
- name: Set up Go ${{ matrix.go-version }}
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
- name: Test
run: go test -v -cover ./...
- name: Build
run: go build -v -ldflags "-s -w" -trimpath

View File

@ -1,41 +0,0 @@
name: Main
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
main:
name: Main Process
runs-on: ubuntu-latest
env:
GO_VERSION: stable
GOLANGCI_LINT_VERSION: v2.0.1
CGO_ENABLED: 0
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
- name: Check and get dependencies
run: |
go mod download
go mod tidy
git diff --exit-code go.mod
git diff --exit-code go.sum
# https://golangci-lint.run/usage/install#other-ci
- name: Install golangci-lint ${{ env.GOLANGCI_LINT_VERSION }}
run: |
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b $(go env GOPATH)/bin ${GOLANGCI_LINT_VERSION}
golangci-lint --version
- name: Make
run: make

View File

@ -1,70 +0,0 @@
name: "Release a tag"
on:
push:
tags:
- v*
jobs:
release:
name: Release Process
runs-on: ubuntu-latest
env:
GO_VERSION: stable
CGO_ENABLED: 0
steps:
# temporary workaround for an error in free disk space action
# https://github.com/jlumbroso/free-disk-space/issues/14
- name: Update Package List and Remove Dotnet
run: |
sudo apt-get update
sudo apt-get remove -y '^dotnet-.*'
# https://github.com/marketplace/actions/free-disk-space-ubuntu
- name: Free Disk Space
uses: jlumbroso/free-disk-space@main
with:
# this might remove tools that are actually needed
tool-cache: false
# all of these default to true
android: true
dotnet: true
haskell: true
large-packages: true
docker-images: true
swap-storage: false
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
- name: dockerhub-login
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: ghcr-login
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
with:
version: latest
args: release -p 1 --clean --timeout=90m
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

2
.gitignore vendored
View File

@ -6,5 +6,3 @@ dumpcerts.sh
acme.json acme.json
acme-backup.json acme-backup.json
traefik-certs-dumper traefik-certs-dumper
manifest.json
/linux-*.Dockerfile

41
.golangci.toml Normal file
View File

@ -0,0 +1,41 @@
[run]
deadline = "2m"
skip-files = []
[linters-settings]
[linters-settings.govet]
check-shadowing = true
[linters-settings.gocyclo]
min-complexity = 12.0
[linters-settings.maligned]
suggest-new = true
[linters-settings.goconst]
min-len = 3.0
min-occurrences = 3.0
[linters-settings.misspell]
locale = "US"
[linters]
enable-all = true
disable = [
"maligned",
"lll",
"gas",
"dupl",
"prealloc",
"scopelint",
]
[issues]
exclude-use-default = false
max-per-linter = 0
max-same-issues = 0
exclude = []
[[issues.exclude-rules]]
path = "version.go"
text = "`(version|commit|date)` is a global variable"

View File

@ -1,108 +0,0 @@
version: "2"
formatters:
enable:
- gci
- gofumpt
settings:
gofumpt:
extra-rules: true
linters:
default: all
disable:
- cyclop # duplicate of gocyclo
- dupl
- err113
- exhaustive
- exhaustruct
- lll
- mnd
- nilnil
- nlreturn
- paralleltest
- prealloc
- rowserrcheck # not relevant (SQL)
- sqlclosecheck # not relevant (SQL)
- testpackage
- tparallel
- varnamelen
- wrapcheck
- wsl
settings:
depguard:
rules:
main:
deny:
- pkg: github.com/instana/testify
desc: not allowed
- pkg: github.com/pkg/errors
desc: Should be replaced by standard lib errors package
forbidigo:
forbid:
- pattern: ^print(ln)?$
- pattern: ^spew\.Print(f|ln)?$
- pattern: ^spew\.Dump$
funlen:
lines: -1
statements: 40
goconst:
min-len: 3
min-occurrences: 3
gocritic:
disabled-checks:
- sloppyReassign
- rangeValCopy
- octalLiteral
- paramTypeCombine # already handle by gofumpt.extra-rules
enabled-tags:
- diagnostic
- style
- performance
settings:
hugeParam:
sizeThreshold: 100
gocyclo:
min-complexity: 12
godox:
keywords:
- FIXME
gomoddirectives:
replace-allow-list:
- github.com/abbot/go-http-auth
- github.com/go-check/check
- github.com/gorilla/mux
- github.com/mailgun/minheap
- github.com/mailgun/multibuf
- github.com/jaguilar/vt100
gosec:
excludes:
- G204 # Subprocess launched with a potential tainted input or cmd arguments
- G301 # Expect directory permissions to be 0750 or less
- G306 # Expect WriteFile permissions to be 0600 or less
govet:
disable:
- fieldalignment
enable-all: true
misspell:
locale: US
exclusions:
presets:
- comments
rules:
- linters:
- gochecknoglobals
- gochecknoinits
path: cmd/
- linters:
- tagalign
path: internal/traefikv[1-3]/
- path: (.+)\.go$
text: 'ST1000: at least one file in a package should have a package comment'
- path: (.+)\.go$
text: 'package-comments: should have a package comment'
issues:
max-issues-per-linter: 0
max-same-issues: 0

View File

@ -1,16 +1,13 @@
version: 2
project_name: traefik-certs-dumper project_name: traefik-certs-dumper
builds: builds:
- binary: traefik-certs-dumper - binary: traefik-certs-dumper
ldflags:
- -s -w -X github.com/ldez/traefik-certs-dumper/cmd.version={{.Version}} -X github.com/ldez/traefik-certs-dumper/cmd.commit={{.ShortCommit}} -X github.com/ldez/traefik-certs-dumper/cmd.date={{.Date}}
env: env:
- CGO_ENABLED=0 - GO111MODULE=on
goos: goos:
- linux
- darwin
- windows - windows
- darwin
- linux
- freebsd - freebsd
- openbsd - openbsd
goarch: goarch:
@ -34,189 +31,17 @@ changelog:
- '^docs:' - '^docs:'
- '^doc:' - '^doc:'
- '^chore:' - '^chore:'
- '^chore\(deps\):'
- '^test:' - '^test:'
- '^tests:' - '^tests:'
archives: archive:
- id: tcd name_template: '{{ .ProjectName }}_v{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm}}v{{ .Arm }}{{ end }}'
name_template: '{{ .ProjectName }}_v{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' format: tar.gz
formats: [ 'tar.gz' ]
format_overrides: format_overrides:
- goos: windows - goos: windows
formats: [ 'zip' ] format: zip
files: files:
- LICENSE - LICENSE
docker_manifests: #release:
- name_template: 'ldez/traefik-certs-dumper:{{ .Tag }}' # disable: true
image_templates:
- 'ldez/traefik-certs-dumper:{{ .Tag }}-amd64'
- 'ldez/traefik-certs-dumper:{{ .Tag }}-arm64'
- 'ldez/traefik-certs-dumper:{{ .Tag }}-armv7'
- 'ldez/traefik-certs-dumper:{{ .Tag }}-armv6'
- 'ldez/traefik-certs-dumper:{{ .Tag }}-386'
- name_template: 'ldez/traefik-certs-dumper:latest'
image_templates:
- 'ldez/traefik-certs-dumper:{{ .Tag }}-amd64'
- 'ldez/traefik-certs-dumper:{{ .Tag }}-arm64'
- 'ldez/traefik-certs-dumper:{{ .Tag }}-armv7'
- 'ldez/traefik-certs-dumper:{{ .Tag }}-armv6'
- 'ldez/traefik-certs-dumper:{{ .Tag }}-386'
- name_template: 'ldez/traefik-certs-dumper:v{{ .Major }}.{{ .Minor }}'
image_templates:
- 'ldez/traefik-certs-dumper:v{{ .Major }}.{{ .Minor }}-amd64'
- 'ldez/traefik-certs-dumper:v{{ .Major }}.{{ .Minor }}-arm64'
- 'ldez/traefik-certs-dumper:v{{ .Major }}.{{ .Minor }}-armv7'
- 'ldez/traefik-certs-dumper:v{{ .Major }}.{{ .Minor }}-armv6'
- 'ldez/traefik-certs-dumper:v{{ .Major }}.{{ .Minor }}-386'
- name_template: 'ghcr.io/ldez/traefik-certs-dumper:{{ .Tag }}'
image_templates:
- 'ghcr.io/ldez/traefik-certs-dumper:{{ .Tag }}-amd64'
- 'ghcr.io/ldez/traefik-certs-dumper:{{ .Tag }}-arm64'
- 'ghcr.io/ldez/traefik-certs-dumper:{{ .Tag }}-armv7'
- 'ghcr.io/ldez/traefik-certs-dumper:{{ .Tag }}-armv6'
- 'ghcr.io/ldez/traefik-certs-dumper:{{ .Tag }}-386'
- name_template: 'ghcr.io/ldez/traefik-certs-dumper:latest'
image_templates:
- 'ghcr.io/ldez/traefik-certs-dumper:{{ .Tag }}-amd64'
- 'ghcr.io/ldez/traefik-certs-dumper:{{ .Tag }}-arm64'
- 'ghcr.io/ldez/traefik-certs-dumper:{{ .Tag }}-armv7'
- 'ghcr.io/ldez/traefik-certs-dumper:{{ .Tag }}-armv6'
- 'ghcr.io/ldez/traefik-certs-dumper:{{ .Tag }}-386'
- name_template: 'ghcr.io/ldez/traefik-certs-dumper:v{{ .Major }}.{{ .Minor }}'
image_templates:
- 'ghcr.io/ldez/traefik-certs-dumper:v{{ .Major }}.{{ .Minor }}-amd64'
- 'ghcr.io/ldez/traefik-certs-dumper:v{{ .Major }}.{{ .Minor }}-arm64'
- 'ghcr.io/ldez/traefik-certs-dumper:v{{ .Major }}.{{ .Minor }}-armv7'
- 'ghcr.io/ldez/traefik-certs-dumper:v{{ .Major }}.{{ .Minor }}-armv6'
- 'ghcr.io/ldez/traefik-certs-dumper:v{{ .Major }}.{{ .Minor }}-386'
dockers:
- use: buildx
goos: linux
goarch: amd64
dockerfile: buildx.Dockerfile
image_templates:
- 'ldez/traefik-certs-dumper:latest-amd64'
- 'ldez/traefik-certs-dumper:{{ .Tag }}-amd64'
- 'ldez/traefik-certs-dumper:v{{ .Major }}.{{ .Minor }}-amd64'
- 'ghcr.io/ldez/traefik-certs-dumper:latest-amd64'
- 'ghcr.io/ldez/traefik-certs-dumper:{{ .Tag }}-amd64'
- 'ghcr.io/ldez/traefik-certs-dumper:v{{ .Major }}.{{ .Minor }}-amd64'
build_flag_templates:
- '--pull'
# https://github.com/opencontainers/image-spec/blob/main/annotations.md#pre-defined-annotation-keys
- '--label=org.opencontainers.image.title={{.ProjectName}}'
- '--label=org.opencontainers.image.description=Dump ACME data from Traefik to certificates'
- '--label=org.opencontainers.image.source={{.GitURL}}'
- '--label=org.opencontainers.image.url={{.GitURL}}'
- '--label=org.opencontainers.image.documentation=https://github.com/ldez/traefik-certs-dumper'
- '--label=org.opencontainers.image.created={{.Date}}'
- '--label=org.opencontainers.image.revision={{.FullCommit}}'
- '--label=org.opencontainers.image.version={{.Version}}'
- '--platform=linux/amd64'
- use: buildx
goos: linux
goarch: arm64
dockerfile: buildx.Dockerfile
image_templates:
- 'ldez/traefik-certs-dumper:latest-arm64'
- 'ldez/traefik-certs-dumper:latest-arm.v8' # only for compatibility with Seihon
- 'ldez/traefik-certs-dumper:{{ .Tag }}-arm64'
- 'ldez/traefik-certs-dumper:{{ .Tag }}-arm.v8' # only for compatibility with Seihon
- 'ldez/traefik-certs-dumper:v{{ .Major }}.{{ .Minor }}-arm64'
- 'ghcr.io/ldez/traefik-certs-dumper:latest-arm64'
- 'ghcr.io/ldez/traefik-certs-dumper:{{ .Tag }}-arm64'
- 'ghcr.io/ldez/traefik-certs-dumper:v{{ .Major }}.{{ .Minor }}-arm64'
build_flag_templates:
- '--pull'
# https://github.com/opencontainers/image-spec/blob/main/annotations.md#pre-defined-annotation-keys
- '--label=org.opencontainers.image.title={{.ProjectName}}'
- '--label=org.opencontainers.image.description=Dump ACME data from Traefik to certificates'
- '--label=org.opencontainers.image.source={{.GitURL}}'
- '--label=org.opencontainers.image.url={{.GitURL}}'
- '--label=org.opencontainers.image.documentation=https://github.com/ldez/traefik-certs-dumper'
- '--label=org.opencontainers.image.created={{.Date}}'
- '--label=org.opencontainers.image.revision={{.FullCommit}}'
- '--label=org.opencontainers.image.version={{.Version}}'
- '--platform=linux/arm64'
- use: buildx
goos: linux
goarch: arm
goarm: '7'
dockerfile: buildx.Dockerfile
image_templates:
- 'ldez/traefik-certs-dumper:latest-armv7'
- 'ldez/traefik-certs-dumper:latest-arm.v7' # only for compatibility with Seihon
- 'ldez/traefik-certs-dumper:{{ .Tag }}-armv7'
- 'ldez/traefik-certs-dumper:{{ .Tag }}-arm.v7' # only for compatibility with Seihon
- 'ldez/traefik-certs-dumper:v{{ .Major }}.{{ .Minor }}-armv7'
- 'ghcr.io/ldez/traefik-certs-dumper:latest-armv7'
- 'ghcr.io/ldez/traefik-certs-dumper:{{ .Tag }}-armv7'
- 'ghcr.io/ldez/traefik-certs-dumper:v{{ .Major }}.{{ .Minor }}-armv7'
build_flag_templates:
- '--pull'
# https://github.com/opencontainers/image-spec/blob/main/annotations.md#pre-defined-annotation-keys
- '--label=org.opencontainers.image.title={{.ProjectName}}'
- '--label=org.opencontainers.image.description=Dump ACME data from Traefik to certificates'
- '--label=org.opencontainers.image.source={{.GitURL}}'
- '--label=org.opencontainers.image.url={{.GitURL}}'
- '--label=org.opencontainers.image.documentation=https://github.com/ldez/traefik-certs-dumper'
- '--label=org.opencontainers.image.created={{.Date}}'
- '--label=org.opencontainers.image.revision={{.FullCommit}}'
- '--label=org.opencontainers.image.version={{.Version}}'
- '--platform=linux/arm/v7'
- use: buildx
goos: linux
goarch: arm
goarm: '6'
dockerfile: buildx.Dockerfile
image_templates:
- 'ldez/traefik-certs-dumper:latest-armv6'
- 'ldez/traefik-certs-dumper:latest-arm.v6' # only for compatibility with Seihon
- 'ldez/traefik-certs-dumper:{{ .Tag }}-armv6'
- 'ldez/traefik-certs-dumper:{{ .Tag }}-arm.v6' # only for compatibility with Seihon
- 'ldez/traefik-certs-dumper:v{{ .Major }}.{{ .Minor }}-armv6'
- 'ghcr.io/ldez/traefik-certs-dumper:latest-armv6'
- 'ghcr.io/ldez/traefik-certs-dumper:{{ .Tag }}-armv6'
- 'ghcr.io/ldez/traefik-certs-dumper:v{{ .Major }}.{{ .Minor }}-armv6'
build_flag_templates:
- '--pull'
# https://github.com/opencontainers/image-spec/blob/main/annotations.md#pre-defined-annotation-keys
- '--label=org.opencontainers.image.title={{.ProjectName}}'
- '--label=org.opencontainers.image.description=Dump ACME data from Traefik to certificates'
- '--label=org.opencontainers.image.source={{.GitURL}}'
- '--label=org.opencontainers.image.url={{.GitURL}}'
- '--label=org.opencontainers.image.documentation=https://github.com/ldez/traefik-certs-dumper'
- '--label=org.opencontainers.image.created={{.Date}}'
- '--label=org.opencontainers.image.revision={{.FullCommit}}'
- '--label=org.opencontainers.image.version={{.Version}}'
- '--platform=linux/arm/v6'
- use: buildx
goos: linux
goarch: '386'
dockerfile: buildx.Dockerfile
image_templates:
- 'ldez/traefik-certs-dumper:latest-386'
- 'ldez/traefik-certs-dumper:{{ .Tag }}-386'
- 'ldez/traefik-certs-dumper:v{{ .Major }}.{{ .Minor }}-386'
- 'ghcr.io/ldez/traefik-certs-dumper:latest-386'
- 'ghcr.io/ldez/traefik-certs-dumper:{{ .Tag }}-386'
- 'ghcr.io/ldez/traefik-certs-dumper:v{{ .Major }}.{{ .Minor }}-386'
build_flag_templates:
- '--pull'
# https://github.com/opencontainers/image-spec/blob/main/annotations.md#pre-defined-annotation-keys
- '--label=org.opencontainers.image.title={{.ProjectName}}'
- '--label=org.opencontainers.image.description=Dump ACME data from Traefik to certificates'
- '--label=org.opencontainers.image.source={{.GitURL}}'
- '--label=org.opencontainers.image.url={{.GitURL}}'
- '--label=org.opencontainers.image.documentation=https://github.com/ldez/traefik-certs-dumper'
- '--label=org.opencontainers.image.created={{.Date}}'
- '--label=org.opencontainers.image.revision={{.FullCommit}}'
- '--label=org.opencontainers.image.version={{.Version}}'
- '--platform=linux/386'

32
.travis.yml Normal file
View File

@ -0,0 +1,32 @@
language: go
go:
- 1.11.x
- 1.x
sudo: false
env:
- GO111MODULE=on
notifications:
email:
on_success: never
on_failure: change
before_install:
# Install linters and misspell
- curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | bash -s -- -b $GOPATH/bin v1.15.0
- golangci-lint --version
install:
- echo "TRAVIS_GO_VERSION=$TRAVIS_GO_VERSION"
- go get
deploy:
- provider: script
skip_cleanup: true
script: curl -sL https://git.io/goreleaser | bash
on:
tags: true
condition: $TRAVIS_GO_VERSION =~ ^1\.x$

View File

@ -1,4 +1,4 @@
Copyright 2019-2024 Fernandez Ludovic Copyright 2019 Fernandez Ludovic
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

View File

@ -1,7 +1,5 @@
.PHONY: default clean checks test build .PHONY: default clean checks test build
export GO111MODULE=on
TAG_NAME := $(shell git tag -l --contains HEAD) TAG_NAME := $(shell git tag -l --contains HEAD)
SHA := $(shell git rev-parse --short HEAD) SHA := $(shell git rev-parse --short HEAD)
VERSION := $(if $(TAG_NAME),$(TAG_NAME),$(SHA)) VERSION := $(if $(TAG_NAME),$(TAG_NAME),$(SHA))
@ -18,10 +16,7 @@ clean:
build: clean build: clean
@echo Version: $(VERSION) $(BUILD_DATE) @echo Version: $(VERSION) $(BUILD_DATE)
go build -v -ldflags '-X "github.com/ldez/traefik-certs-dumper/cmd.version=${VERSION}" -X "github.com/ldez/traefik-certs-dumper/cmd.commit=${SHA}" -X "github.com/ldez/traefik-certs-dumper/cmd.date=${BUILD_DATE}"' -o traefik-certs-dumper go build -v -ldflags '-X "main.version=${VERSION}" -X "main.commit=${SHA}" -X "main.date=${BUILD_DATE}"'
checks: checks:
golangci-lint run golangci-lint run
doc:
go run . doc

View File

@ -1,9 +0,0 @@
# syntax=docker/dockerfile:1.4
FROM alpine:3
RUN apk --no-cache --no-progress add git ca-certificates tzdata jq \
&& rm -rf /var/cache/apk/*
COPY traefik-certs-dumper /usr/bin/traefik-certs-dumper
ENTRYPOINT ["/usr/bin/traefik-certs-dumper"]

View File

@ -1,50 +0,0 @@
package cmd
import (
"context"
"time"
"github.com/kvtools/boltdb"
"github.com/ldez/traefik-certs-dumper/v2/dumper"
"github.com/ldez/traefik-certs-dumper/v2/dumper/kv"
"github.com/spf13/cobra"
)
// boltdbCmd represents the boltdb command.
var boltdbCmd = &cobra.Command{
Use: "boltdb",
Short: "Dump the content of BoltDB.",
Long: `Dump the content of BoltDB.`,
RunE: runE(boltdbRun),
}
func init() {
kvCmd.AddCommand(boltdbCmd)
boltdbCmd.Flags().Bool("persist-connection", false, "Persist connection for boltdb.")
boltdbCmd.Flags().String("bucket", "traefik", "Bucket for boltdb.")
}
func boltdbRun(baseConfig *dumper.BaseConfig, cmd *cobra.Command) error {
config, err := getKvConfig(cmd)
if err != nil {
return err
}
connectionTimeout, err := cmd.Flags().GetInt("connection-timeout")
if err != nil {
return err
}
persistConnection, _ := cmd.Flags().GetBool("persist-connection")
config.Options = &boltdb.Config{
Bucket: cmd.Flag("bucket").Value.String(),
PersistConnection: persistConnection,
ConnectionTimeout: time.Duration(connectionTimeout) * time.Second,
}
config.StoreName = boltdb.StoreName
return kv.Dump(context.Background(), config, baseConfig)
}

View File

@ -1,53 +0,0 @@
package cmd
import (
"context"
"time"
"github.com/kvtools/consul"
"github.com/ldez/traefik-certs-dumper/v2/dumper"
"github.com/ldez/traefik-certs-dumper/v2/dumper/kv"
"github.com/spf13/cobra"
)
// consulCmd represents the consul command.
var consulCmd = &cobra.Command{
Use: "consul",
Short: "Dump the content of Consul.",
Long: `Dump the content of Consul.`,
RunE: runE(consulRun),
}
func init() {
kvCmd.AddCommand(consulCmd)
consulCmd.Flags().String("token", "", "Token for consul.")
}
func consulRun(baseConfig *dumper.BaseConfig, cmd *cobra.Command) error {
config, err := getKvConfig(cmd)
if err != nil {
return err
}
tlsConfig, err := createTLSConfig(cmd)
if err != nil {
return err
}
connectionTimeout, err := cmd.Flags().GetInt("connection-timeout")
if err != nil {
return err
}
config.Options = &consul.Config{
TLS: tlsConfig,
ConnectionTimeout: time.Duration(connectionTimeout) * time.Second,
Token: cmd.Flag("token").Value.String(),
Namespace: "",
}
config.StoreName = consul.StoreName
return kv.Dump(context.Background(), config, baseConfig)
}

View File

@ -1,20 +0,0 @@
package cmd
import (
"github.com/spf13/cobra"
"github.com/spf13/cobra/doc"
)
// docCmd represents the doc command.
var docCmd = &cobra.Command{
Use: "doc",
Short: "Generate documentation",
Hidden: true,
RunE: func(_ *cobra.Command, _ []string) error {
return doc.GenMarkdownTree(rootCmd, "./docs")
},
}
func init() {
rootCmd.AddCommand(docCmd)
}

View File

@ -1,78 +0,0 @@
package cmd
import (
"context"
"time"
"github.com/kvtools/etcdv2"
"github.com/kvtools/etcdv3"
"github.com/ldez/traefik-certs-dumper/v2/dumper"
"github.com/ldez/traefik-certs-dumper/v2/dumper/kv"
"github.com/spf13/cobra"
)
// etcdCmd represents the etcd command.
var etcdCmd = &cobra.Command{
Use: "etcd",
Short: "Dump the content of etcd.",
Long: `Dump the content of etcd.`,
RunE: runE(etcdRun),
}
func init() {
kvCmd.AddCommand(etcdCmd)
etcdCmd.Flags().Int("sync-period", 0, "Sync period for etcd in seconds.")
etcdCmd.Flags().String("etcd-version", "etcd", "The etcd version can be: 'etcd' or 'etcdv3'.")
}
func etcdRun(baseConfig *dumper.BaseConfig, cmd *cobra.Command) error {
config, err := getKvConfig(cmd)
if err != nil {
return err
}
backend, err := cmd.Flags().GetString("etcd-version")
if err != nil {
return err
}
tlsConfig, err := createTLSConfig(cmd)
if err != nil {
return err
}
synPeriod, err := cmd.Flags().GetInt("sync-period")
if err != nil {
return err
}
connectionTimeout, err := cmd.Flags().GetInt("connection-timeout")
if err != nil {
return err
}
switch backend {
case "etcdv3":
config.Options = &etcdv3.Config{
TLS: tlsConfig,
ConnectionTimeout: time.Duration(connectionTimeout) * time.Second,
SyncPeriod: time.Duration(synPeriod) * time.Second,
Username: cmd.Flag("password").Value.String(),
Password: cmd.Flag("username").Value.String(),
}
config.StoreName = etcdv3.StoreName
default:
config.Options = &etcdv2.Config{
TLS: tlsConfig,
ConnectionTimeout: time.Duration(connectionTimeout) * time.Second,
SyncPeriod: time.Duration(synPeriod) * time.Second,
Username: cmd.Flag("password").Value.String(),
Password: cmd.Flag("username").Value.String(),
}
config.StoreName = etcdv2.StoreName
}
return kv.Dump(context.Background(), config, baseConfig)
}

View File

@ -1,29 +0,0 @@
package cmd
import (
"context"
"github.com/ldez/traefik-certs-dumper/v2/dumper"
"github.com/ldez/traefik-certs-dumper/v2/dumper/file"
"github.com/spf13/cobra"
)
var fileCmd = &cobra.Command{
Use: "file",
Short: `Dump the content of the "acme.json" file.`,
Long: `Dump the content of the "acme.json" file from Traefik to certificates.`,
RunE: runE(func(baseConfig *dumper.BaseConfig, cmd *cobra.Command) error {
acmeFile := cmd.Flag("source").Value.String()
baseConfig.Version = cmd.Flag("version").Value.String()
return file.Dump(context.Background(), acmeFile, baseConfig)
}),
}
func init() {
rootCmd.AddCommand(fileCmd)
fileCmd.Flags().String("source", "./acme.json", "Path to 'acme.json' file.")
fileCmd.Flags().String("version", "", "Traefik version. If empty use v1. Possible values: 'v2', 'v3'.")
}

162
cmd/kv.go
View File

@ -1,162 +0,0 @@
package cmd
import (
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"os"
"path/filepath"
"github.com/ldez/traefik-certs-dumper/v2/dumper/kv"
"github.com/spf13/cobra"
)
// kvCmd represents the kv command.
var kvCmd = &cobra.Command{
Use: "kv",
Short: `Dump the content of a KV store.`,
Long: `Dump the content of a KV store.`,
}
func init() {
rootCmd.AddCommand(kvCmd)
kvCmd.PersistentFlags().StringSlice("endpoints", []string{"localhost:8500"}, "List of endpoints.")
kvCmd.PersistentFlags().Int("connection-timeout", 0, "Connection timeout in seconds.")
kvCmd.PersistentFlags().String("prefix", "traefik", "Prefix used for KV store.")
kvCmd.PersistentFlags().String("suffix", kv.DefaultStoreKeySuffix, "Suffix/Storage used for KV store.")
kvCmd.PersistentFlags().String("password", "", "Password for connection.")
kvCmd.PersistentFlags().String("username", "", "Username for connection.")
kvCmd.PersistentFlags().Bool("tls", false, "Enable TLS encryption.")
kvCmd.PersistentFlags().String("tls.ca", "", "Root CA for certificate verification if TLS is enabled")
kvCmd.PersistentFlags().Bool("tls.ca.optional", false, "")
kvCmd.PersistentFlags().String("tls.cert", "", "TLS cert")
kvCmd.PersistentFlags().String("tls.key", "", "TLS key")
kvCmd.PersistentFlags().Bool("tls.insecureskipverify", false, "Trust unverified certificates if TLS is enabled.")
}
func getKvConfig(cmd *cobra.Command) (*kv.Config, error) {
endpoints, err := cmd.Flags().GetStringSlice("endpoints")
if err != nil {
return nil, err
}
return &kv.Config{
Endpoints: endpoints,
Prefix: cmd.Flag("prefix").Value.String(),
Suffix: cmd.Flag("suffix").Value.String(),
}, nil
}
func createTLSConfig(cmd *cobra.Command) (*tls.Config, error) {
enable, _ := cmd.Flags().GetBool("tls")
if !enable {
return nil, nil
}
ca := cmd.Flag("tls.ca").Value.String()
caPool, err := getCertPool(ca)
if err != nil {
return nil, err
}
caOptional, _ := cmd.Flags().GetBool("tls.ca.optional")
clientAuth := getClientAuth(ca, caOptional)
insecureSkipVerify, _ := cmd.Flags().GetBool("tls.insecureskipverify")
privateKey := cmd.Flag("tls.key").Value.String()
certContent := cmd.Flag("tls.cert").Value.String()
if !insecureSkipVerify && (certContent == "" || privateKey == "") {
return nil, errors.New("TLS Certificate or Key file must be set when TLS configuration is created")
}
cert, err := getCertificate(privateKey, certContent)
if err != nil {
return nil, fmt.Errorf("failed to load TLS keypair: %w", err)
}
return &tls.Config{
Certificates: []tls.Certificate{cert},
RootCAs: caPool,
InsecureSkipVerify: insecureSkipVerify, //nolint:gosec // it's a CLI option.
ClientAuth: clientAuth,
}, nil
}
func getCertPool(ca string) (*x509.CertPool, error) {
caPool := x509.NewCertPool()
if ca != "" {
caContent, err := getCAContent(ca)
if err != nil {
return nil, fmt.Errorf("failed to read CA. %w", err)
}
if !caPool.AppendCertsFromPEM(caContent) {
return nil, errors.New("failed to parse CA")
}
}
return caPool, nil
}
func getCAContent(ca string) ([]byte, error) {
if _, err := os.Stat(ca); err != nil {
if os.IsNotExist(err) {
return []byte(ca), nil
}
return nil, err
}
caContent, err := os.ReadFile(filepath.Clean(ca))
if err != nil {
return nil, err
}
return caContent, nil
}
func getClientAuth(ca string, caOptional bool) tls.ClientAuthType {
if ca == "" {
return tls.NoClientCert
}
if caOptional {
return tls.VerifyClientCertIfGiven
}
return tls.RequireAndVerifyClientCert
}
func getCertificate(privateKey, certContent string) (tls.Certificate, error) {
if certContent == "" || privateKey == "" {
return tls.Certificate{}, nil
}
_, errKeyIsFile := os.Stat(privateKey)
_, errCertIsFile := os.Stat(certContent)
if errCertIsFile == nil && os.IsNotExist(errKeyIsFile) {
return tls.Certificate{}, errors.New("tls cert is a file, but tls key is not")
}
if os.IsNotExist(errCertIsFile) && errKeyIsFile == nil {
return tls.Certificate{}, errors.New("TLS key is a file, but tls cert is not")
}
// string
if os.IsNotExist(errCertIsFile) && os.IsNotExist(errKeyIsFile) {
return tls.X509KeyPair([]byte(certContent), []byte(privateKey))
}
// files
if errCertIsFile == nil && errKeyIsFile == nil {
return tls.LoadX509KeyPair(certContent, privateKey)
}
if errCertIsFile != nil {
return tls.Certificate{}, errCertIsFile
}
return tls.Certificate{}, errKeyIsFile
}

View File

@ -1,217 +0,0 @@
package cmd
import (
"crypto/rand"
"fmt"
"log"
"math/big"
"os"
"path/filepath"
"strconv"
"time"
"github.com/charmbracelet/lipgloss"
"github.com/ldez/traefik-certs-dumper/v2/dumper"
"github.com/mitchellh/go-homedir"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var cfgFile string
// rootCmd represents the base command when called without any subcommands.
var rootCmd = &cobra.Command{
Use: "traefik-certs-dumper",
Short: "Dump Let's Encrypt certificates from Traefik.",
Long: `Dump Let's Encrypt certificates from Traefik.`,
PersistentPreRunE: func(cmd *cobra.Command, _ []string) error {
if cmd.Name() == "version" {
return nil
}
crtExt := cmd.Flag("crt-ext").Value.String()
keyExt := cmd.Flag("key-ext").Value.String()
subDir, _ := strconv.ParseBool(cmd.Flag("domain-subdir").Value.String())
if !subDir {
if crtExt == keyExt {
return fmt.Errorf("--crt-ext (%q) and --key-ext (%q) are identical, in this case --domain-subdir is required", crtExt, keyExt)
}
}
return nil
},
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
if err := rootCmd.Execute(); err != nil {
log.Println(err)
os.Exit(1)
}
help()
}
func init() {
cobra.OnInitialize(initConfig)
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.traefik-certs-dumper.yaml)")
rootCmd.PersistentFlags().String("dest", "./dump", "Path to store the dump content.")
rootCmd.PersistentFlags().String("crt-ext", ".crt", "The file extension of the generated certificates.")
rootCmd.PersistentFlags().String("crt-name", "certificate", "The file name (without extension) of the generated certificates.")
rootCmd.PersistentFlags().String("key-ext", ".key", "The file extension of the generated private keys.")
rootCmd.PersistentFlags().String("key-name", "privatekey", "The file name (without extension) of the generated private keys.")
rootCmd.PersistentFlags().Bool("domain-subdir", false, "Use domain as sub-directory.")
rootCmd.PersistentFlags().Bool("clean", true, "Clean destination folder before dumping content.")
rootCmd.PersistentFlags().Bool("watch", false, "Enable watching changes.")
rootCmd.PersistentFlags().String("post-hook", "", "Execute a command only if changes occurs on the data source. (works only with the watch mode)")
}
// initConfig reads in config file and ENV variables if set.
func initConfig() {
if cfgFile != "" {
// Use config file from the flag.
viper.SetConfigFile(cfgFile)
} else {
// Find home directory.
home, err := homedir.Dir()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
// Search config in home directory with name ".traefik-certs-dumper" (without extension).
viper.AddConfigPath(home)
viper.SetConfigName(".traefik-certs-dumper")
}
viper.AutomaticEnv() // read in environment variables that match
// If a config file is found, read it in.
if err := viper.ReadInConfig(); err == nil {
fmt.Println("Using config file:", viper.ConfigFileUsed())
}
}
func runE(apply func(*dumper.BaseConfig, *cobra.Command) error) func(*cobra.Command, []string) error {
return func(cmd *cobra.Command, _ []string) error {
baseConfig, err := getBaseConfig(cmd)
if err != nil {
return err
}
err = apply(baseConfig, cmd)
if err != nil {
return err
}
return tree(baseConfig.DumpPath, "")
}
}
func tree(root, indent string) error {
fi, err := os.Stat(root)
if err != nil {
return fmt.Errorf("could not stat %s: %w", root, err)
}
fmt.Println(fi.Name())
if !fi.IsDir() {
return nil
}
fis, err := os.ReadDir(root)
if err != nil {
return fmt.Errorf("could not read dir %s: %w", root, err)
}
var names []string
for _, fi := range fis {
if fi.Name()[0] != '.' {
names = append(names, fi.Name())
}
}
for i, name := range names {
add := "│ "
if i == len(names)-1 {
fmt.Print(indent + "└──")
add = " "
} else {
fmt.Print(indent + "├──")
}
if err := tree(filepath.Join(root, name), indent+add); err != nil {
return err
}
}
return nil
}
func getBaseConfig(cmd *cobra.Command) (*dumper.BaseConfig, error) {
subDir, err := strconv.ParseBool(cmd.Flag("domain-subdir").Value.String())
if err != nil {
return nil, err
}
clean, err := strconv.ParseBool(cmd.Flag("clean").Value.String())
if err != nil {
return nil, err
}
watch, err := strconv.ParseBool(cmd.Flag("watch").Value.String())
if err != nil {
return nil, err
}
return &dumper.BaseConfig{
DumpPath: cmd.Flag("dest").Value.String(),
CrtInfo: dumper.FileInfo{
Name: cmd.Flag("crt-name").Value.String(),
Ext: cmd.Flag("crt-ext").Value.String(),
},
KeyInfo: dumper.FileInfo{
Name: cmd.Flag("key-name").Value.String(),
Ext: cmd.Flag("key-ext").Value.String(),
},
DomainSubDir: subDir,
Clean: clean,
Watch: watch,
Hook: cmd.Flag("post-hook").Value.String(),
}, nil
}
func help() {
var maxInt int64 = 2 // -> 50%
if time.Now().Month() == time.December {
maxInt = 1 // -> 100%
}
n, _ := rand.Int(rand.Reader, big.NewInt(maxInt))
if n.Cmp(big.NewInt(0)) != 0 {
return
}
log.SetFlags(0)
pStyle := lipgloss.NewStyle().
Padding(1).
BorderStyle(lipgloss.RoundedBorder()).
BorderForeground(lipgloss.Color("161")).
Align(lipgloss.Center)
hStyle := lipgloss.NewStyle().Bold(true)
s := fmt.Sprintln(hStyle.Render("Request for Donation."))
s += `
I need your help!
Donations fund the maintenance and development of traefik-certs-dumper.
Click on this link to donate: https://donate.ldez.dev`
log.Println(pStyle.Render(s))
log.SetFlags(log.LstdFlags | log.Lshortfile)
}

View File

@ -1,46 +0,0 @@
package cmd
import (
"context"
"time"
"github.com/kvtools/zookeeper"
"github.com/ldez/traefik-certs-dumper/v2/dumper"
"github.com/ldez/traefik-certs-dumper/v2/dumper/kv"
"github.com/spf13/cobra"
)
// zookeeperCmd represents the zookeeper command.
var zookeeperCmd = &cobra.Command{
Use: "zookeeper",
Short: "Dump the content of zookeeper.",
Long: `Dump the content of zookeeper.`,
RunE: runE(zookeeperRun),
}
func init() {
kvCmd.AddCommand(zookeeperCmd)
}
func zookeeperRun(baseConfig *dumper.BaseConfig, cmd *cobra.Command) error {
config, err := getKvConfig(cmd)
if err != nil {
return err
}
connectionTimeout, err := cmd.Flags().GetInt("connection-timeout")
if err != nil {
return err
}
config.Options = &zookeeper.Config{
ConnectionTimeout: time.Duration(connectionTimeout) * time.Second,
Username: cmd.Flag("password").Value.String(),
Password: cmd.Flag("username").Value.String(),
MaxBufferSize: 0,
}
config.StoreName = zookeeper.StoreName
return kv.Dump(context.Background(), config, baseConfig)
}

View File

@ -1 +0,0 @@
This directory content external contributions that are not maintain by @ldez.

View File

@ -1,40 +0,0 @@
[Unit]
Description=traefik certs dumper
; If you do not start traefik via systemd, choose network.target or docker.target
After=traefik.target
Wants=network-online.target systemd-networkd-wait-online.service
[Service]
Restart=on-abnormal
User=root
ExecStart=/usr/local/bin/traefik-certs-dumper file --version v2 --source /etc/traefik/acme/acme.json --dest /etc/ssl --watch
RestartSec=30
TimeoutSec=30
;WatchdogSec=30
; Limit the number of file descriptors; see `man systemd.exec` for more limit settings.
; LimitNOFILE=1048576
; Limit number of processes in this unit
LimitNPROC=1
; Use private /tmp and /var/tmp, which are discarded after traefik stops.
PrivateTmp=true
; Use a minimal /dev (May bring additional security if switched to 'true', but it may not work on Raspberry Pis or other devices)
PrivateDevices=true
; Hide /home, /root, and /run/user. Nobody will steal your SSH-keys.
ProtectHome=true
; Make cgroups /sys/fs/cgroup read-only
ProtectControlGroups=true
; Make kernel settings (procfs and sysfs) read-only
ProtectKernelTunables=true
; Make /usr, /boot, /etc and possibly some more folders read-only.
ProtectSystem=full
; This merely retains r/w access rights, it does not add any new. Must still be writable on the host!
ReadWriteDirectories=/etc/ssl
ReadOnlyPaths=/etc/traefik/acme/acme.json
; The following additional security directives only work with systemd v229 or later.
NoNewPrivileges=true
[Install]
WantedBy=multi-user.target

View File

@ -1,42 +0,0 @@
services:
traefik:
image: traefik:v1.7
command:
- --logLevel=INFO
- --defaultEntryPoints=web,websecure
- "--entryPoints=Name:web Address::80 Redirect.EntryPoint:websecure"
- "--entryPoints=Name:websecure Address::443 TLS"
- --docker
- --docker.exposedByDefault=false
- --acme
- --acme.email=email@example.com
- --acme.caServer=https://acme-staging-v02.api.letsencrypt.org/directory
- --acme.entrypoint=websecure
- --acme.storage=/letsencrypt/acme.json
- --acme.onHostRule
- --acme.tlsChallenge
ports:
- 80:80
- 443:443
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./letsencrypt:/letsencrypt
traefik-certs-dumper:
image: ldez/traefik-certs-dumper:v2.9.3
entrypoint: sh -c '
while ! [ -e /data/acme.json ]
|| ! [ `jq ".Certificates | length" /data/acme.json` != 0 ]; do
sleep 1
; done
&& traefik-certs-dumper file --watch
--source /data/acme.json --dest /data/certs'
volumes:
- ./letsencrypt:/data
network_mode: "none"
whoami:
image: traefik/whoami:v1.8.1
labels:
traefik.enable: true
traefik.frontend.rule: Host:example.com

View File

@ -1,44 +0,0 @@
services:
traefik:
image: traefik:v2.11.3
command:
- --log.level=INFO
- --entrypoints.web.address=:80
- --entrypoints.web.http.redirections.entrypoint.to=websecure
- --entrypoints.web.http.redirections.entrypoint.scheme=https
- --entrypoints.websecure.address=:443
- --entrypoints.websecure.http.tls=true
- --entrypoints.websecure.http.tls.certResolver=le
- --providers.docker.exposedbydefault=false
- --certificatesresolvers.le.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory
- --certificatesresolvers.le.acme.email=email@example.com
- --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json
- --certificatesresolvers.le.acme.tlsChallenge=true
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./letsencrypt/:/letsencrypt
traefik-certs-dumper:
image: ldez/traefik-certs-dumper:v2.9.3
entrypoint: sh -c '
while ! [ -e /data/acme.json ]
|| ! [ `jq ".[] | .Certificates | length" /data/acme.json | jq -s "add" ` != 0 ]; do
sleep 1
; done
&& traefik-certs-dumper file --version v2 --watch
--source /data/acme.json --dest /data/certs'
volumes:
- ./letsencrypt:/data
network_mode: "none"
whoami:
image: traefik/whoami:v1.8.1
labels:
traefik.enable: 'true'
traefik.http.routers.app.rule: Host(`example.com`)
traefik.http.routers.app.entrypoints: websecure

View File

@ -1,32 +0,0 @@
## traefik-certs-dumper
Dump Let's Encrypt certificates from Traefik.
### Synopsis
Dump Let's Encrypt certificates from Traefik.
### Options
```
--clean Clean destination folder before dumping content. (default true)
--config string config file (default is $HOME/.traefik-certs-dumper.yaml)
--crt-ext string The file extension of the generated certificates. (default ".crt")
--crt-name string The file name (without extension) of the generated certificates. (default "certificate")
--dest string Path to store the dump content. (default "./dump")
--domain-subdir Use domain as sub-directory.
-h, --help help for traefik-certs-dumper
--key-ext string The file extension of the generated private keys. (default ".key")
--key-name string The file name (without extension) of the generated private keys. (default "privatekey")
--post-hook string Execute a command only if changes occurs on the data source. (works only with the watch mode)
--watch Enable watching changes.
```
### SEE ALSO
* [traefik-certs-dumper completion](traefik-certs-dumper_completion.md) - Generate the autocompletion script for the specified shell
* [traefik-certs-dumper file](traefik-certs-dumper_file.md) - Dump the content of the "acme.json" file.
* [traefik-certs-dumper kv](traefik-certs-dumper_kv.md) - Dump the content of a KV store.
* [traefik-certs-dumper version](traefik-certs-dumper_version.md) - Display version
###### Auto generated by spf13/cobra on 21-Feb-2025

View File

@ -1,40 +0,0 @@
## traefik-certs-dumper completion
Generate the autocompletion script for the specified shell
### Synopsis
Generate the autocompletion script for traefik-certs-dumper for the specified shell.
See each sub-command's help for details on how to use the generated script.
### Options
```
-h, --help help for completion
```
### Options inherited from parent commands
```
--clean Clean destination folder before dumping content. (default true)
--config string config file (default is $HOME/.traefik-certs-dumper.yaml)
--crt-ext string The file extension of the generated certificates. (default ".crt")
--crt-name string The file name (without extension) of the generated certificates. (default "certificate")
--dest string Path to store the dump content. (default "./dump")
--domain-subdir Use domain as sub-directory.
--key-ext string The file extension of the generated private keys. (default ".key")
--key-name string The file name (without extension) of the generated private keys. (default "privatekey")
--post-hook string Execute a command only if changes occurs on the data source. (works only with the watch mode)
--watch Enable watching changes.
```
### SEE ALSO
* [traefik-certs-dumper](traefik-certs-dumper.md) - Dump Let's Encrypt certificates from Traefik.
* [traefik-certs-dumper completion bash](traefik-certs-dumper_completion_bash.md) - Generate the autocompletion script for bash
* [traefik-certs-dumper completion fish](traefik-certs-dumper_completion_fish.md) - Generate the autocompletion script for fish
* [traefik-certs-dumper completion powershell](traefik-certs-dumper_completion_powershell.md) - Generate the autocompletion script for powershell
* [traefik-certs-dumper completion zsh](traefik-certs-dumper_completion_zsh.md) - Generate the autocompletion script for zsh
###### Auto generated by spf13/cobra on 21-Feb-2025

View File

@ -1,59 +0,0 @@
## traefik-certs-dumper completion bash
Generate the autocompletion script for bash
### Synopsis
Generate the autocompletion script for the bash shell.
This script depends on the 'bash-completion' package.
If it is not installed already, you can install it via your OS's package manager.
To load completions in your current shell session:
source <(traefik-certs-dumper completion bash)
To load completions for every new session, execute once:
#### Linux:
traefik-certs-dumper completion bash > /etc/bash_completion.d/traefik-certs-dumper
#### macOS:
traefik-certs-dumper completion bash > $(brew --prefix)/etc/bash_completion.d/traefik-certs-dumper
You will need to start a new shell for this setup to take effect.
```
traefik-certs-dumper completion bash
```
### Options
```
-h, --help help for bash
--no-descriptions disable completion descriptions
```
### Options inherited from parent commands
```
--clean Clean destination folder before dumping content. (default true)
--config string config file (default is $HOME/.traefik-certs-dumper.yaml)
--crt-ext string The file extension of the generated certificates. (default ".crt")
--crt-name string The file name (without extension) of the generated certificates. (default "certificate")
--dest string Path to store the dump content. (default "./dump")
--domain-subdir Use domain as sub-directory.
--key-ext string The file extension of the generated private keys. (default ".key")
--key-name string The file name (without extension) of the generated private keys. (default "privatekey")
--post-hook string Execute a command only if changes occurs on the data source. (works only with the watch mode)
--watch Enable watching changes.
```
### SEE ALSO
* [traefik-certs-dumper completion](traefik-certs-dumper_completion.md) - Generate the autocompletion script for the specified shell
###### Auto generated by spf13/cobra on 21-Feb-2025

View File

@ -1,50 +0,0 @@
## traefik-certs-dumper completion fish
Generate the autocompletion script for fish
### Synopsis
Generate the autocompletion script for the fish shell.
To load completions in your current shell session:
traefik-certs-dumper completion fish | source
To load completions for every new session, execute once:
traefik-certs-dumper completion fish > ~/.config/fish/completions/traefik-certs-dumper.fish
You will need to start a new shell for this setup to take effect.
```
traefik-certs-dumper completion fish [flags]
```
### Options
```
-h, --help help for fish
--no-descriptions disable completion descriptions
```
### Options inherited from parent commands
```
--clean Clean destination folder before dumping content. (default true)
--config string config file (default is $HOME/.traefik-certs-dumper.yaml)
--crt-ext string The file extension of the generated certificates. (default ".crt")
--crt-name string The file name (without extension) of the generated certificates. (default "certificate")
--dest string Path to store the dump content. (default "./dump")
--domain-subdir Use domain as sub-directory.
--key-ext string The file extension of the generated private keys. (default ".key")
--key-name string The file name (without extension) of the generated private keys. (default "privatekey")
--post-hook string Execute a command only if changes occurs on the data source. (works only with the watch mode)
--watch Enable watching changes.
```
### SEE ALSO
* [traefik-certs-dumper completion](traefik-certs-dumper_completion.md) - Generate the autocompletion script for the specified shell
###### Auto generated by spf13/cobra on 21-Feb-2025

View File

@ -1,47 +0,0 @@
## traefik-certs-dumper completion powershell
Generate the autocompletion script for powershell
### Synopsis
Generate the autocompletion script for powershell.
To load completions in your current shell session:
traefik-certs-dumper completion powershell | Out-String | Invoke-Expression
To load completions for every new session, add the output of the above command
to your powershell profile.
```
traefik-certs-dumper completion powershell [flags]
```
### Options
```
-h, --help help for powershell
--no-descriptions disable completion descriptions
```
### Options inherited from parent commands
```
--clean Clean destination folder before dumping content. (default true)
--config string config file (default is $HOME/.traefik-certs-dumper.yaml)
--crt-ext string The file extension of the generated certificates. (default ".crt")
--crt-name string The file name (without extension) of the generated certificates. (default "certificate")
--dest string Path to store the dump content. (default "./dump")
--domain-subdir Use domain as sub-directory.
--key-ext string The file extension of the generated private keys. (default ".key")
--key-name string The file name (without extension) of the generated private keys. (default "privatekey")
--post-hook string Execute a command only if changes occurs on the data source. (works only with the watch mode)
--watch Enable watching changes.
```
### SEE ALSO
* [traefik-certs-dumper completion](traefik-certs-dumper_completion.md) - Generate the autocompletion script for the specified shell
###### Auto generated by spf13/cobra on 21-Feb-2025

View File

@ -1,61 +0,0 @@
## traefik-certs-dumper completion zsh
Generate the autocompletion script for zsh
### Synopsis
Generate the autocompletion script for the zsh shell.
If shell completion is not already enabled in your environment you will need
to enable it. You can execute the following once:
echo "autoload -U compinit; compinit" >> ~/.zshrc
To load completions in your current shell session:
source <(traefik-certs-dumper completion zsh)
To load completions for every new session, execute once:
#### Linux:
traefik-certs-dumper completion zsh > "${fpath[1]}/_traefik-certs-dumper"
#### macOS:
traefik-certs-dumper completion zsh > $(brew --prefix)/share/zsh/site-functions/_traefik-certs-dumper
You will need to start a new shell for this setup to take effect.
```
traefik-certs-dumper completion zsh [flags]
```
### Options
```
-h, --help help for zsh
--no-descriptions disable completion descriptions
```
### Options inherited from parent commands
```
--clean Clean destination folder before dumping content. (default true)
--config string config file (default is $HOME/.traefik-certs-dumper.yaml)
--crt-ext string The file extension of the generated certificates. (default ".crt")
--crt-name string The file name (without extension) of the generated certificates. (default "certificate")
--dest string Path to store the dump content. (default "./dump")
--domain-subdir Use domain as sub-directory.
--key-ext string The file extension of the generated private keys. (default ".key")
--key-name string The file name (without extension) of the generated private keys. (default "privatekey")
--post-hook string Execute a command only if changes occurs on the data source. (works only with the watch mode)
--watch Enable watching changes.
```
### SEE ALSO
* [traefik-certs-dumper completion](traefik-certs-dumper_completion.md) - Generate the autocompletion script for the specified shell
###### Auto generated by spf13/cobra on 21-Feb-2025

View File

@ -1,40 +0,0 @@
## traefik-certs-dumper file
Dump the content of the "acme.json" file.
### Synopsis
Dump the content of the "acme.json" file from Traefik to certificates.
```
traefik-certs-dumper file [flags]
```
### Options
```
-h, --help help for file
--source string Path to 'acme.json' file. (default "./acme.json")
--version string Traefik version. If empty use v1. Possible values: 'v2', 'v3'.
```
### Options inherited from parent commands
```
--clean Clean destination folder before dumping content. (default true)
--config string config file (default is $HOME/.traefik-certs-dumper.yaml)
--crt-ext string The file extension of the generated certificates. (default ".crt")
--crt-name string The file name (without extension) of the generated certificates. (default "certificate")
--dest string Path to store the dump content. (default "./dump")
--domain-subdir Use domain as sub-directory.
--key-ext string The file extension of the generated private keys. (default ".key")
--key-name string The file name (without extension) of the generated private keys. (default "privatekey")
--post-hook string Execute a command only if changes occurs on the data source. (works only with the watch mode)
--watch Enable watching changes.
```
### SEE ALSO
* [traefik-certs-dumper](traefik-certs-dumper.md) - Dump Let's Encrypt certificates from Traefik.
###### Auto generated by spf13/cobra on 21-Feb-2025

View File

@ -1,50 +0,0 @@
## traefik-certs-dumper kv
Dump the content of a KV store.
### Synopsis
Dump the content of a KV store.
### Options
```
--connection-timeout int Connection timeout in seconds.
--endpoints strings List of endpoints. (default [localhost:8500])
-h, --help help for kv
--password string Password for connection.
--prefix string Prefix used for KV store. (default "traefik")
--suffix string Suffix/Storage used for KV store. (default "/acme/account/object")
--tls Enable TLS encryption.
--tls.ca string Root CA for certificate verification if TLS is enabled
--tls.ca.optional
--tls.cert string TLS cert
--tls.insecureskipverify Trust unverified certificates if TLS is enabled.
--tls.key string TLS key
--username string Username for connection.
```
### Options inherited from parent commands
```
--clean Clean destination folder before dumping content. (default true)
--config string config file (default is $HOME/.traefik-certs-dumper.yaml)
--crt-ext string The file extension of the generated certificates. (default ".crt")
--crt-name string The file name (without extension) of the generated certificates. (default "certificate")
--dest string Path to store the dump content. (default "./dump")
--domain-subdir Use domain as sub-directory.
--key-ext string The file extension of the generated private keys. (default ".key")
--key-name string The file name (without extension) of the generated private keys. (default "privatekey")
--post-hook string Execute a command only if changes occurs on the data source. (works only with the watch mode)
--watch Enable watching changes.
```
### SEE ALSO
* [traefik-certs-dumper](traefik-certs-dumper.md) - Dump Let's Encrypt certificates from Traefik.
* [traefik-certs-dumper kv boltdb](traefik-certs-dumper_kv_boltdb.md) - Dump the content of BoltDB.
* [traefik-certs-dumper kv consul](traefik-certs-dumper_kv_consul.md) - Dump the content of Consul.
* [traefik-certs-dumper kv etcd](traefik-certs-dumper_kv_etcd.md) - Dump the content of etcd.
* [traefik-certs-dumper kv zookeeper](traefik-certs-dumper_kv_zookeeper.md) - Dump the content of zookeeper.
###### Auto generated by spf13/cobra on 21-Feb-2025

View File

@ -1,52 +0,0 @@
## traefik-certs-dumper kv boltdb
Dump the content of BoltDB.
### Synopsis
Dump the content of BoltDB.
```
traefik-certs-dumper kv boltdb [flags]
```
### Options
```
--bucket string Bucket for boltdb. (default "traefik")
-h, --help help for boltdb
--persist-connection Persist connection for boltdb.
```
### Options inherited from parent commands
```
--clean Clean destination folder before dumping content. (default true)
--config string config file (default is $HOME/.traefik-certs-dumper.yaml)
--connection-timeout int Connection timeout in seconds.
--crt-ext string The file extension of the generated certificates. (default ".crt")
--crt-name string The file name (without extension) of the generated certificates. (default "certificate")
--dest string Path to store the dump content. (default "./dump")
--domain-subdir Use domain as sub-directory.
--endpoints strings List of endpoints. (default [localhost:8500])
--key-ext string The file extension of the generated private keys. (default ".key")
--key-name string The file name (without extension) of the generated private keys. (default "privatekey")
--password string Password for connection.
--post-hook string Execute a command only if changes occurs on the data source. (works only with the watch mode)
--prefix string Prefix used for KV store. (default "traefik")
--suffix string Suffix/Storage used for KV store. (default "/acme/account/object")
--tls Enable TLS encryption.
--tls.ca string Root CA for certificate verification if TLS is enabled
--tls.ca.optional
--tls.cert string TLS cert
--tls.insecureskipverify Trust unverified certificates if TLS is enabled.
--tls.key string TLS key
--username string Username for connection.
--watch Enable watching changes.
```
### SEE ALSO
* [traefik-certs-dumper kv](traefik-certs-dumper_kv.md) - Dump the content of a KV store.
###### Auto generated by spf13/cobra on 21-Feb-2025

View File

@ -1,51 +0,0 @@
## traefik-certs-dumper kv consul
Dump the content of Consul.
### Synopsis
Dump the content of Consul.
```
traefik-certs-dumper kv consul [flags]
```
### Options
```
-h, --help help for consul
--token string Token for consul.
```
### Options inherited from parent commands
```
--clean Clean destination folder before dumping content. (default true)
--config string config file (default is $HOME/.traefik-certs-dumper.yaml)
--connection-timeout int Connection timeout in seconds.
--crt-ext string The file extension of the generated certificates. (default ".crt")
--crt-name string The file name (without extension) of the generated certificates. (default "certificate")
--dest string Path to store the dump content. (default "./dump")
--domain-subdir Use domain as sub-directory.
--endpoints strings List of endpoints. (default [localhost:8500])
--key-ext string The file extension of the generated private keys. (default ".key")
--key-name string The file name (without extension) of the generated private keys. (default "privatekey")
--password string Password for connection.
--post-hook string Execute a command only if changes occurs on the data source. (works only with the watch mode)
--prefix string Prefix used for KV store. (default "traefik")
--suffix string Suffix/Storage used for KV store. (default "/acme/account/object")
--tls Enable TLS encryption.
--tls.ca string Root CA for certificate verification if TLS is enabled
--tls.ca.optional
--tls.cert string TLS cert
--tls.insecureskipverify Trust unverified certificates if TLS is enabled.
--tls.key string TLS key
--username string Username for connection.
--watch Enable watching changes.
```
### SEE ALSO
* [traefik-certs-dumper kv](traefik-certs-dumper_kv.md) - Dump the content of a KV store.
###### Auto generated by spf13/cobra on 21-Feb-2025

View File

@ -1,52 +0,0 @@
## traefik-certs-dumper kv etcd
Dump the content of etcd.
### Synopsis
Dump the content of etcd.
```
traefik-certs-dumper kv etcd [flags]
```
### Options
```
--etcd-version string The etcd version can be: 'etcd' or 'etcdv3'. (default "etcd")
-h, --help help for etcd
--sync-period int Sync period for etcd in seconds.
```
### Options inherited from parent commands
```
--clean Clean destination folder before dumping content. (default true)
--config string config file (default is $HOME/.traefik-certs-dumper.yaml)
--connection-timeout int Connection timeout in seconds.
--crt-ext string The file extension of the generated certificates. (default ".crt")
--crt-name string The file name (without extension) of the generated certificates. (default "certificate")
--dest string Path to store the dump content. (default "./dump")
--domain-subdir Use domain as sub-directory.
--endpoints strings List of endpoints. (default [localhost:8500])
--key-ext string The file extension of the generated private keys. (default ".key")
--key-name string The file name (without extension) of the generated private keys. (default "privatekey")
--password string Password for connection.
--post-hook string Execute a command only if changes occurs on the data source. (works only with the watch mode)
--prefix string Prefix used for KV store. (default "traefik")
--suffix string Suffix/Storage used for KV store. (default "/acme/account/object")
--tls Enable TLS encryption.
--tls.ca string Root CA for certificate verification if TLS is enabled
--tls.ca.optional
--tls.cert string TLS cert
--tls.insecureskipverify Trust unverified certificates if TLS is enabled.
--tls.key string TLS key
--username string Username for connection.
--watch Enable watching changes.
```
### SEE ALSO
* [traefik-certs-dumper kv](traefik-certs-dumper_kv.md) - Dump the content of a KV store.
###### Auto generated by spf13/cobra on 21-Feb-2025

View File

@ -1,50 +0,0 @@
## traefik-certs-dumper kv zookeeper
Dump the content of zookeeper.
### Synopsis
Dump the content of zookeeper.
```
traefik-certs-dumper kv zookeeper [flags]
```
### Options
```
-h, --help help for zookeeper
```
### Options inherited from parent commands
```
--clean Clean destination folder before dumping content. (default true)
--config string config file (default is $HOME/.traefik-certs-dumper.yaml)
--connection-timeout int Connection timeout in seconds.
--crt-ext string The file extension of the generated certificates. (default ".crt")
--crt-name string The file name (without extension) of the generated certificates. (default "certificate")
--dest string Path to store the dump content. (default "./dump")
--domain-subdir Use domain as sub-directory.
--endpoints strings List of endpoints. (default [localhost:8500])
--key-ext string The file extension of the generated private keys. (default ".key")
--key-name string The file name (without extension) of the generated private keys. (default "privatekey")
--password string Password for connection.
--post-hook string Execute a command only if changes occurs on the data source. (works only with the watch mode)
--prefix string Prefix used for KV store. (default "traefik")
--suffix string Suffix/Storage used for KV store. (default "/acme/account/object")
--tls Enable TLS encryption.
--tls.ca string Root CA for certificate verification if TLS is enabled
--tls.ca.optional
--tls.cert string TLS cert
--tls.insecureskipverify Trust unverified certificates if TLS is enabled.
--tls.key string TLS key
--username string Username for connection.
--watch Enable watching changes.
```
### SEE ALSO
* [traefik-certs-dumper kv](traefik-certs-dumper_kv.md) - Dump the content of a KV store.
###### Auto generated by spf13/cobra on 21-Feb-2025

View File

@ -1,34 +0,0 @@
## traefik-certs-dumper version
Display version
```
traefik-certs-dumper version [flags]
```
### Options
```
-h, --help help for version
```
### Options inherited from parent commands
```
--clean Clean destination folder before dumping content. (default true)
--config string config file (default is $HOME/.traefik-certs-dumper.yaml)
--crt-ext string The file extension of the generated certificates. (default ".crt")
--crt-name string The file name (without extension) of the generated certificates. (default "certificate")
--dest string Path to store the dump content. (default "./dump")
--domain-subdir Use domain as sub-directory.
--key-ext string The file extension of the generated private keys. (default ".key")
--key-name string The file name (without extension) of the generated private keys. (default "privatekey")
--post-hook string Execute a command only if changes occurs on the data source. (works only with the watch mode)
--watch Enable watching changes.
```
### SEE ALSO
* [traefik-certs-dumper](traefik-certs-dumper.md) - Dump Let's Encrypt certificates from Traefik.
###### Auto generated by spf13/cobra on 21-Feb-2025

136
dumper.go Normal file
View File

@ -0,0 +1,136 @@
package main
import (
"encoding/json"
"encoding/pem"
"io/ioutil"
"os"
"path/filepath"
"github.com/xenolf/lego/certcrypto"
"github.com/xenolf/lego/registration"
)
const (
certsSubDir = "certs"
keysSubDir = "private"
)
// StoredData represents the data managed by the Store
type StoredData struct {
Account *Account
Certificates []*Certificate
HTTPChallenges map[string]map[string][]byte
TLSChallenges map[string]*Certificate
}
// Certificate is a struct which contains all data needed from an ACME certificate
type Certificate struct {
Domain Domain
Certificate []byte
Key []byte
}
// Domain holds a domain name with SANs
type Domain struct {
Main string
SANs []string
}
// Account is used to store lets encrypt registration info
type Account struct {
Email string
Registration *registration.Resource
PrivateKey []byte
KeyType certcrypto.KeyType
}
func dump(acmeFile, dumpPath string, crtExt, keyExt string, domainSubDir bool) error {
f, err := os.Open(acmeFile)
if err != nil {
return err
}
data := StoredData{}
if err = json.NewDecoder(f).Decode(&data); err != nil {
return err
}
if err = os.RemoveAll(dumpPath); err != nil {
return err
}
if !domainSubDir {
if err = os.MkdirAll(filepath.Join(dumpPath, certsSubDir), 0755); err != nil {
return err
}
}
if err = os.MkdirAll(filepath.Join(dumpPath, keysSubDir), 0755); err != nil {
return err
}
privateKeyPem := extractPEMPrivateKey(data.Account)
err = ioutil.WriteFile(filepath.Join(dumpPath, keysSubDir, "letsencrypt"+keyExt), privateKeyPem, 0666)
if err != nil {
return err
}
for _, cert := range data.Certificates {
err := writeCert(dumpPath, cert, crtExt, domainSubDir)
if err != nil {
return err
}
err = writeKey(dumpPath, cert, keyExt, domainSubDir)
if err != nil {
return err
}
}
return nil
}
func writeCert(dumpPath string, cert *Certificate, ext string, domainSubDir bool) error {
certPath := filepath.Join(dumpPath, keysSubDir, cert.Domain.Main+ext)
if domainSubDir {
certPath = filepath.Join(dumpPath, cert.Domain.Main, "certificate"+ext)
if err := os.MkdirAll(filepath.Join(dumpPath, cert.Domain.Main), 0755); err != nil {
return err
}
}
return ioutil.WriteFile(certPath, cert.Key, 0666)
}
func writeKey(dumpPath string, cert *Certificate, ext string, domainSubDir bool) error {
keyPath := filepath.Join(dumpPath, certsSubDir, cert.Domain.Main+ext)
if domainSubDir {
keyPath = filepath.Join(dumpPath, cert.Domain.Main, "privatekey"+ext)
if err := os.MkdirAll(filepath.Join(dumpPath, cert.Domain.Main), 0755); err != nil {
return err
}
}
return ioutil.WriteFile(keyPath, cert.Certificate, 0666)
}
func extractPEMPrivateKey(account *Account) []byte {
var block *pem.Block
switch account.KeyType {
case certcrypto.RSA2048, certcrypto.RSA4096, certcrypto.RSA8192:
block = &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: account.PrivateKey,
}
case certcrypto.EC256, certcrypto.EC384:
block = &pem.Block{
Type: "EC PRIVATE KEY",
Bytes: account.PrivateKey,
}
default:
panic("unsupported key type")
}
return pem.EncodeToMemory(block)
}

View File

@ -1,19 +0,0 @@
package dumper
// BaseConfig Base dump command configuration.
type BaseConfig struct {
DumpPath string
CrtInfo FileInfo
KeyInfo FileInfo
DomainSubDir bool
Clean bool
Watch bool
Hook string
Version string
}
// FileInfo File information.
type FileInfo struct {
Name string
Ext string
}

View File

@ -1,242 +0,0 @@
package file
import (
"bytes"
"context"
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"os"
"path/filepath"
"strings"
"github.com/fsnotify/fsnotify"
"github.com/ldez/traefik-certs-dumper/v2/dumper"
dumperv1 "github.com/ldez/traefik-certs-dumper/v2/dumper/v1"
dumperv2 "github.com/ldez/traefik-certs-dumper/v2/dumper/v2"
dumperv3 "github.com/ldez/traefik-certs-dumper/v2/dumper/v3"
"github.com/ldez/traefik-certs-dumper/v2/hook"
"github.com/ldez/traefik-certs-dumper/v2/internal/traefikv1"
"github.com/ldez/traefik-certs-dumper/v2/internal/traefikv2"
"github.com/ldez/traefik-certs-dumper/v2/internal/traefikv3"
)
// Dump Dumps "acme.json" file to certificates.
func Dump(ctx context.Context, acmeFile string, baseConfig *dumper.BaseConfig) error {
err := dump(acmeFile, baseConfig)
if err != nil {
return err
}
if baseConfig.Watch {
hook.Exec(ctx, baseConfig.Hook)
return watch(ctx, acmeFile, baseConfig)
}
return nil
}
func dump(acmeFile string, baseConfig *dumper.BaseConfig) error {
switch baseConfig.Version {
case "v3":
err := dumpV3(acmeFile, baseConfig)
if err != nil {
return fmt.Errorf("v3: dump failed: %w", err)
}
return nil
case "v2":
err := dumpV2(acmeFile, baseConfig)
if err != nil {
return fmt.Errorf("v2: dump failed: %w", err)
}
return nil
case "v1":
err := dumpV1(acmeFile, baseConfig)
if err != nil {
return fmt.Errorf("v1: dump failed: %w", err)
}
return nil
default:
err := dumpV1(acmeFile, baseConfig)
if err != nil {
return fmt.Errorf("v1: dump failed: %w", err)
}
return nil
}
}
func dumpV1(acmeFile string, baseConfig *dumper.BaseConfig) error {
data := &traefikv1.StoredData{}
err := readJSONFile(acmeFile, data)
if err != nil {
return err
}
return dumperv1.Dump(data, baseConfig)
}
func dumpV2(acmeFile string, baseConfig *dumper.BaseConfig) error {
data := map[string]*traefikv2.StoredData{}
err := readJSONFile(acmeFile, &data)
if err != nil {
return err
}
return dumperv2.Dump(data, baseConfig)
}
func dumpV3(acmeFile string, baseConfig *dumper.BaseConfig) error {
data := map[string]*traefikv3.StoredData{}
err := readJSONFile(acmeFile, &data)
if err != nil {
return err
}
return dumperv3.Dump(data, baseConfig)
}
func readJSONFile(acmeFile string, data interface{}) error {
source, err := os.Open(filepath.Clean(acmeFile))
if err != nil {
return fmt.Errorf("failed to open file %q: %w", acmeFile, err)
}
defer func() { _ = source.Close() }()
err = json.NewDecoder(source).Decode(data)
if errors.Is(err, io.EOF) {
log.Printf("warn: file %q may not be ready: %v", acmeFile, err)
return nil
}
if err != nil {
return fmt.Errorf("failed to unmarshal file %q: %w", acmeFile, err)
}
return nil
}
func watch(ctx context.Context, acmeFile string, baseConfig *dumper.BaseConfig) error {
watcher, err := fsnotify.NewWatcher()
if err != nil {
return fmt.Errorf("failed to create new watcher: %w", err)
}
defer func() { _ = watcher.Close() }()
done := make(chan bool)
go func() {
var previousHash []byte
for {
select {
case event, ok := <-watcher.Events:
if !ok {
return
}
if isDebug() {
log.Println("event:", event)
}
hash, errW := manageEvent(ctx, watcher, event, acmeFile, previousHash, baseConfig)
if errW != nil {
log.Println("error:", errW)
done <- true
return
}
previousHash = hash
case errW, ok := <-watcher.Errors:
if !ok {
return
}
log.Println("error:", errW)
done <- true
return
}
}
}()
err = watcher.Add(acmeFile)
if err != nil {
return fmt.Errorf("failed to add a new watcher: %w", err)
}
<-done
return nil
}
func manageEvent(ctx context.Context, watcher *fsnotify.Watcher, event fsnotify.Event, acmeFile string, previousHash []byte, baseConfig *dumper.BaseConfig) ([]byte, error) {
err := manageRename(watcher, event, acmeFile)
if err != nil {
return nil, fmt.Errorf("watcher renewal failed: %w", err)
}
hash, err := calculateHash(acmeFile)
if err != nil {
return nil, fmt.Errorf("file hash calculation failed: %w", err)
}
if !bytes.Equal(previousHash, hash) {
if isDebug() {
log.Println("detected changes on file:", event.Name)
}
if errD := dump(acmeFile, baseConfig); errD != nil {
return nil, errD
}
if isDebug() {
log.Println("Dumped new certificate data.")
}
hook.Exec(ctx, baseConfig.Hook)
}
return hash, nil
}
func manageRename(watcher *fsnotify.Watcher, event fsnotify.Event, acmeFile string) error {
if event.Op&fsnotify.Rename != fsnotify.Rename {
return nil
}
if err := watcher.Remove(acmeFile); err != nil {
return err
}
return watcher.Add(acmeFile)
}
func calculateHash(acmeFile string) ([]byte, error) {
file, err := os.Open(filepath.Clean(acmeFile))
if err != nil {
return nil, err
}
defer func() { _ = file.Close() }()
h := sha256.New()
_, err = io.Copy(h, file)
if err != nil {
return nil, err
}
return h.Sum(nil), nil
}
func isDebug() bool {
return strings.EqualFold(os.Getenv("TCD_DEBUG"), "true")
}

View File

@ -1,60 +0,0 @@
package file
import (
"testing"
"github.com/ldez/traefik-certs-dumper/v2/dumper"
"github.com/stretchr/testify/require"
)
func TestDump(t *testing.T) {
testCases := []struct {
desc string
acmeFile string
version string
}{
{
desc: "should skip EOF error",
acmeFile: "./fixtures/acme-empty.json",
},
{
desc: "should dump traefik v1 file content",
acmeFile: "./fixtures/acme-v1.json",
},
{
desc: "should dump traefik v2 file content",
acmeFile: "./fixtures/acme-v2.json",
version: "v2",
},
{
desc: "should dump traefik v3 file content",
acmeFile: "./fixtures/acme-v3.json",
version: "v3",
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
dir := t.TempDir()
cfg := &dumper.BaseConfig{
DumpPath: dir,
CrtInfo: dumper.FileInfo{
Name: "certificate",
Ext: ".crt",
},
KeyInfo: dumper.FileInfo{
Name: "privatekey",
Ext: ".key",
},
Clean: true,
Version: test.version,
}
err := Dump(t.Context(), test.acmeFile, cfg)
require.NoError(t, err)
})
}
}

View File

@ -1,12 +0,0 @@
{
"Certificates": [
{
"domain": {
"main": "test.example.com"
},
"certificate": "Q2VydGlmaWNhdGU=",
"key": "Q2VydGlmaWNhdGUgS2V5",
"Store": "default"
}
]
}

View File

@ -1,36 +0,0 @@
{
"default": {
"Account": {
"Email": "test@email.com",
"Registration": {
"body": {
"status": "valid",
"contact": [
"mailto:test@email.com"
]
},
"uri": "https://acme-v02.api.letsencrypt.org/acme/acct/12345678"
},
"PrivateKey": "Q2VydGlmaWNhdGUgS2V5",
"KeyType": "4096"
},
"Certificates": [
{
"domain": {
"main": "my.domain.com"
},
"certificate": "Q2VydGlmaWNhdGU=",
"key": "Q2VydGlmaWNhdGUgS2V5",
"Store": "default"
},
{
"domain": {
"main": "my.domain2.com"
},
"certificate": "Q2VydGlmaWNhdGU=",
"key": "Q2VydGlmaWNhdGUgS2V5",
"Store": "default"
}
]
}
}

View File

@ -1,36 +0,0 @@
{
"default": {
"Account": {
"Email": "test@email.com",
"Registration": {
"body": {
"status": "valid",
"contact": [
"mailto:test@email.com"
]
},
"uri": "https://acme-v02.api.letsencrypt.org/acme/acct/12345678"
},
"PrivateKey": "Q2VydGlmaWNhdGUgS2V5",
"KeyType": "4096"
},
"Certificates": [
{
"domain": {
"main": "my.domain.com"
},
"certificate": "Q2VydGlmaWNhdGU=",
"key": "Q2VydGlmaWNhdGUgS2V5",
"Store": "default"
},
{
"domain": {
"main": "my.domain2.com"
},
"certificate": "Q2VydGlmaWNhdGU=",
"key": "Q2VydGlmaWNhdGUgS2V5",
"Store": "default"
}
]
}
}

View File

@ -1,14 +0,0 @@
package kv
import (
"github.com/kvtools/valkeyrie"
)
// Config KV configuration.
type Config struct {
StoreName string
Prefix string
Suffix string
Endpoints []string
Options valkeyrie.Config
}

View File

@ -1,68 +0,0 @@
package kv
import (
"github.com/go-acme/lego/v4/certcrypto"
"github.com/go-acme/lego/v4/registration"
"github.com/ldez/traefik-certs-dumper/v2/internal/traefikv1"
)
// CertificateOld is used to store certificate info.
type CertificateOld struct {
Domain string
CertURL string
CertStableURL string
PrivateKey []byte
Certificate []byte
}
// AccountOld is used to store lets encrypt registration info.
type AccountOld struct {
Email string
Registration *registration.Resource
PrivateKey []byte
KeyType certcrypto.KeyType
DomainsCertificate DomainsCertificates
ChallengeCerts map[string]*ChallengeCert
HTTPChallenge map[string]map[string][]byte
}
// DomainsCertificates stores a certificate for multiple domains.
type DomainsCertificates struct {
Certs []*DomainsCertificate
}
// ChallengeCert stores a challenge certificate.
type ChallengeCert struct {
Certificate []byte
PrivateKey []byte
}
// DomainsCertificate contains a certificate for multiple domains.
type DomainsCertificate struct {
Domains traefikv1.Domain
Certificate *CertificateOld
}
// convertOldAccount converts account information from old account format.
func convertOldAccount(account *AccountOld) *traefikv1.StoredData {
storedData := &traefikv1.StoredData{
Account: &traefikv1.Account{
PrivateKey: account.PrivateKey,
Registration: account.Registration,
Email: account.Email,
KeyType: account.KeyType,
},
}
var certs []*traefikv1.Certificate
for _, oldCert := range account.DomainsCertificate.Certs {
certs = append(certs, &traefikv1.Certificate{
Certificate: oldCert.Certificate.Certificate,
Domain: oldCert.Domains,
Key: oldCert.Certificate.PrivateKey,
})
}
storedData.Certificates = certs
return storedData
}

View File

@ -1,102 +0,0 @@
package kv
import (
"bytes"
"compress/gzip"
"context"
"encoding/json"
"fmt"
"io"
"log"
"os"
"strings"
"github.com/kvtools/valkeyrie"
"github.com/kvtools/valkeyrie/store"
"github.com/ldez/traefik-certs-dumper/v2/dumper"
v1 "github.com/ldez/traefik-certs-dumper/v2/dumper/v1"
"github.com/ldez/traefik-certs-dumper/v2/hook"
"github.com/ldez/traefik-certs-dumper/v2/internal/traefikv1"
)
// DefaultStoreKeySuffix is the default suffix/storage.
const DefaultStoreKeySuffix = "/acme/account/object"
// Dump Dumps KV content to certificates.
func Dump(ctx context.Context, config *Config, baseConfig *dumper.BaseConfig) error {
kvStore, err := valkeyrie.NewStore(ctx, config.StoreName, config.Endpoints, config.Options)
if err != nil {
return fmt.Errorf("unable to create client of the store: %w", err)
}
storeKey := config.Prefix + config.Suffix
if baseConfig.Watch {
return watch(ctx, kvStore, storeKey, baseConfig)
}
pair, err := kvStore.Get(ctx, storeKey, nil)
if err != nil {
return fmt.Errorf("unable to retrieve %s value: %w", storeKey, err)
}
return dumpPair(pair, baseConfig)
}
func watch(ctx context.Context, kvStore store.Store, storeKey string, baseConfig *dumper.BaseConfig) error {
pairs, err := kvStore.Watch(ctx, storeKey, nil)
if err != nil {
return err
}
for {
pair := <-pairs
if pair == nil {
return fmt.Errorf("could not fetch Key/Value pair for key %v", storeKey)
}
err = dumpPair(pair, baseConfig)
if err != nil {
return err
}
if isDebug() {
log.Println("Dumped new certificate data.")
}
hook.Exec(ctx, baseConfig.Hook)
}
}
func dumpPair(pair *store.KVPair, baseConfig *dumper.BaseConfig) error {
data, err := getStoredDataFromGzip(pair)
if err != nil {
return err
}
return v1.Dump(data, baseConfig)
}
func getStoredDataFromGzip(pair *store.KVPair) (*traefikv1.StoredData, error) {
reader, err := gzip.NewReader(bytes.NewBuffer(pair.Value))
if err != nil {
return nil, fmt.Errorf("fail to create GZip reader: %w", err)
}
acmeData, err := io.ReadAll(reader)
if err != nil {
return nil, fmt.Errorf("unable to read the pair content: %w", err)
}
account := &AccountOld{}
//nolint:musttag // old format
if err := json.Unmarshal(acmeData, &account); err != nil {
return nil, fmt.Errorf("unable marshal AccountOld: %w", err)
}
return convertOldAccount(account), nil
}
func isDebug() bool {
return strings.EqualFold(os.Getenv("TCD_DEBUG"), "true")
}

View File

@ -1,124 +0,0 @@
package v1
import (
"encoding/pem"
"fmt"
"os"
"path/filepath"
"github.com/go-acme/lego/v4/certcrypto"
"github.com/ldez/traefik-certs-dumper/v2/dumper"
"github.com/ldez/traefik-certs-dumper/v2/internal/traefikv1"
)
const (
certsSubDir = "certs"
keysSubDir = "private"
)
// Dump Dumps data to certificates.
func Dump(data *traefikv1.StoredData, baseConfig *dumper.BaseConfig) error {
if baseConfig.Clean {
err := cleanDir(baseConfig.DumpPath)
if err != nil {
return fmt.Errorf("folder cleaning failed: %w", err)
}
}
if !baseConfig.DomainSubDir {
if err := os.MkdirAll(filepath.Join(baseConfig.DumpPath, certsSubDir), 0o755); err != nil {
return fmt.Errorf("certs folder creation failure: %w", err)
}
}
if err := os.MkdirAll(filepath.Join(baseConfig.DumpPath, keysSubDir), 0o755); err != nil {
return fmt.Errorf("keys folder creation failure: %w", err)
}
for _, cert := range data.Certificates {
err := writeCert(baseConfig.DumpPath, cert, baseConfig.CrtInfo, baseConfig.DomainSubDir)
if err != nil {
return fmt.Errorf("failed to write certificates: %w", err)
}
err = writeKey(baseConfig.DumpPath, cert, baseConfig.KeyInfo, baseConfig.DomainSubDir)
if err != nil {
return fmt.Errorf("failed to write certificate keys: %w", err)
}
}
if data.Account == nil {
return nil
}
privateKeyPem := extractPEMPrivateKey(data.Account)
return os.WriteFile(filepath.Join(baseConfig.DumpPath, keysSubDir, "letsencrypt"+baseConfig.KeyInfo.Ext), privateKeyPem, 0o600)
}
func writeCert(dumpPath string, cert *traefikv1.Certificate, info dumper.FileInfo, domainSubDir bool) error {
certPath := filepath.Join(dumpPath, certsSubDir, safeName(cert.Domain.Main+info.Ext))
if domainSubDir {
certPath = filepath.Join(dumpPath, safeName(cert.Domain.Main), info.Name+info.Ext)
if err := os.MkdirAll(filepath.Join(dumpPath, safeName(cert.Domain.Main)), 0o755); err != nil {
return err
}
}
return os.WriteFile(certPath, cert.Certificate, 0o666)
}
func writeKey(dumpPath string, cert *traefikv1.Certificate, info dumper.FileInfo, domainSubDir bool) error {
keyPath := filepath.Join(dumpPath, keysSubDir, safeName(cert.Domain.Main+info.Ext))
if domainSubDir {
keyPath = filepath.Join(dumpPath, safeName(cert.Domain.Main), info.Name+info.Ext)
if err := os.MkdirAll(filepath.Join(dumpPath, safeName(cert.Domain.Main)), 0o755); err != nil {
return err
}
}
return os.WriteFile(keyPath, cert.Key, 0o600)
}
func extractPEMPrivateKey(account *traefikv1.Account) []byte {
var block *pem.Block
switch account.KeyType {
case certcrypto.RSA2048, certcrypto.RSA4096, certcrypto.RSA8192:
block = &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: account.PrivateKey,
}
case certcrypto.EC256, certcrypto.EC384:
block = &pem.Block{
Type: "EC PRIVATE KEY",
Bytes: account.PrivateKey,
}
default:
panic(fmt.Sprintf("unsupported key type: '%v'", account.KeyType))
}
return pem.EncodeToMemory(block)
}
func cleanDir(dumpPath string) error {
_, errExists := os.Stat(dumpPath)
if os.IsNotExist(errExists) {
return nil
}
if errExists != nil {
return errExists
}
dir, err := os.ReadDir(dumpPath)
if err != nil {
return err
}
for _, f := range dir {
if err := os.RemoveAll(filepath.Join(dumpPath, f.Name())); err != nil {
return err
}
}
return nil
}

View File

@ -1,8 +0,0 @@
//go:build !windows
// +build !windows
package v1
func safeName(filename string) string {
return filename
}

View File

@ -1,9 +0,0 @@
// +build windows
package v1
import "strings"
func safeName(filename string) string {
return strings.ReplaceAll(filename, "*", "_")
}

View File

@ -1,132 +0,0 @@
package v2
import (
"encoding/pem"
"fmt"
"os"
"path/filepath"
"github.com/go-acme/lego/v4/certcrypto"
"github.com/ldez/traefik-certs-dumper/v2/dumper"
"github.com/ldez/traefik-certs-dumper/v2/internal/traefikv2"
)
const (
certsSubDir = "certs"
keysSubDir = "private"
)
// Dump Dumps data to certificates.
func Dump(data map[string]*traefikv2.StoredData, baseConfig *dumper.BaseConfig) error {
if baseConfig.Clean {
err := cleanDir(baseConfig.DumpPath)
if err != nil {
return fmt.Errorf("folder cleaning failed: %w", err)
}
}
if !baseConfig.DomainSubDir {
if err := os.MkdirAll(filepath.Join(baseConfig.DumpPath, certsSubDir), 0o755); err != nil {
return fmt.Errorf("certs folder creation failure: %w", err)
}
}
if err := os.MkdirAll(filepath.Join(baseConfig.DumpPath, keysSubDir), 0o755); err != nil {
return fmt.Errorf("keys folder creation failure: %w", err)
}
for _, store := range data {
for _, cert := range store.Certificates {
err := writeCert(baseConfig.DumpPath, cert.Certificate, baseConfig.CrtInfo, baseConfig.DomainSubDir)
if err != nil {
return fmt.Errorf("failed to write certificates: %w", err)
}
err = writeKey(baseConfig.DumpPath, cert.Certificate, baseConfig.KeyInfo, baseConfig.DomainSubDir)
if err != nil {
return fmt.Errorf("failed to write certificate keys: %w", err)
}
}
if store.Account == nil {
continue
}
privateKeyPem := extractPEMPrivateKey(store.Account)
err := os.WriteFile(filepath.Join(baseConfig.DumpPath, keysSubDir, "letsencrypt"+baseConfig.KeyInfo.Ext), privateKeyPem, 0o600)
if err != nil {
return fmt.Errorf("failed to write private key: %w", err)
}
}
return nil
}
func writeCert(dumpPath string, cert traefikv2.Certificate, info dumper.FileInfo, domainSubDir bool) error {
certPath := filepath.Join(dumpPath, certsSubDir, safeName(cert.Domain.Main+info.Ext))
if domainSubDir {
certPath = filepath.Join(dumpPath, safeName(cert.Domain.Main), info.Name+info.Ext)
if err := os.MkdirAll(filepath.Join(dumpPath, safeName(cert.Domain.Main)), 0o755); err != nil {
return err
}
}
return os.WriteFile(certPath, cert.Certificate, 0o666)
}
func writeKey(dumpPath string, cert traefikv2.Certificate, info dumper.FileInfo, domainSubDir bool) error {
keyPath := filepath.Join(dumpPath, keysSubDir, safeName(cert.Domain.Main+info.Ext))
if domainSubDir {
keyPath = filepath.Join(dumpPath, safeName(cert.Domain.Main), info.Name+info.Ext)
if err := os.MkdirAll(filepath.Join(dumpPath, safeName(cert.Domain.Main)), 0o755); err != nil {
return err
}
}
return os.WriteFile(keyPath, cert.Key, 0o600)
}
func extractPEMPrivateKey(account *traefikv2.Account) []byte {
var block *pem.Block
switch account.KeyType {
case certcrypto.RSA2048, certcrypto.RSA4096, certcrypto.RSA8192:
block = &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: account.PrivateKey,
}
case certcrypto.EC256, certcrypto.EC384:
block = &pem.Block{
Type: "EC PRIVATE KEY",
Bytes: account.PrivateKey,
}
default:
panic(fmt.Sprintf("unsupported key type: '%v'", account.KeyType))
}
return pem.EncodeToMemory(block)
}
func cleanDir(dumpPath string) error {
_, errExists := os.Stat(dumpPath)
if os.IsNotExist(errExists) {
return nil
}
if errExists != nil {
return errExists
}
dir, err := os.ReadDir(dumpPath)
if err != nil {
return err
}
for _, f := range dir {
if err := os.RemoveAll(filepath.Join(dumpPath, f.Name())); err != nil {
return err
}
}
return nil
}

View File

@ -1,8 +0,0 @@
//go:build !windows
// +build !windows
package v2
func safeName(filename string) string {
return filename
}

View File

@ -1,9 +0,0 @@
// +build windows
package v2
import "strings"
func safeName(filename string) string {
return strings.ReplaceAll(filename, "*", "_")
}

View File

@ -1,132 +0,0 @@
package v3
import (
"encoding/pem"
"fmt"
"os"
"path/filepath"
"github.com/go-acme/lego/v4/certcrypto"
"github.com/ldez/traefik-certs-dumper/v2/dumper"
"github.com/ldez/traefik-certs-dumper/v2/internal/traefikv3"
)
const (
certsSubDir = "certs"
keysSubDir = "private"
)
// Dump Dumps data to certificates.
func Dump(data map[string]*traefikv3.StoredData, baseConfig *dumper.BaseConfig) error {
if baseConfig.Clean {
err := cleanDir(baseConfig.DumpPath)
if err != nil {
return fmt.Errorf("folder cleaning failed: %w", err)
}
}
if !baseConfig.DomainSubDir {
if err := os.MkdirAll(filepath.Join(baseConfig.DumpPath, certsSubDir), 0o755); err != nil {
return fmt.Errorf("certs folder creation failure: %w", err)
}
}
if err := os.MkdirAll(filepath.Join(baseConfig.DumpPath, keysSubDir), 0o755); err != nil {
return fmt.Errorf("keys folder creation failure: %w", err)
}
for _, store := range data {
for _, cert := range store.Certificates {
err := writeCert(baseConfig.DumpPath, cert.Certificate, baseConfig.CrtInfo, baseConfig.DomainSubDir)
if err != nil {
return fmt.Errorf("failed to write certificates: %w", err)
}
err = writeKey(baseConfig.DumpPath, cert.Certificate, baseConfig.KeyInfo, baseConfig.DomainSubDir)
if err != nil {
return fmt.Errorf("failed to write certificate keys: %w", err)
}
}
if store.Account == nil {
continue
}
privateKeyPem := extractPEMPrivateKey(store.Account)
err := os.WriteFile(filepath.Join(baseConfig.DumpPath, keysSubDir, "letsencrypt"+baseConfig.KeyInfo.Ext), privateKeyPem, 0o600)
if err != nil {
return fmt.Errorf("failed to write private key: %w", err)
}
}
return nil
}
func writeCert(dumpPath string, cert traefikv3.Certificate, info dumper.FileInfo, domainSubDir bool) error {
certPath := filepath.Join(dumpPath, certsSubDir, safeName(cert.Domain.Main+info.Ext))
if domainSubDir {
certPath = filepath.Join(dumpPath, safeName(cert.Domain.Main), info.Name+info.Ext)
if err := os.MkdirAll(filepath.Join(dumpPath, safeName(cert.Domain.Main)), 0o755); err != nil {
return err
}
}
return os.WriteFile(certPath, cert.Certificate, 0o666)
}
func writeKey(dumpPath string, cert traefikv3.Certificate, info dumper.FileInfo, domainSubDir bool) error {
keyPath := filepath.Join(dumpPath, keysSubDir, safeName(cert.Domain.Main+info.Ext))
if domainSubDir {
keyPath = filepath.Join(dumpPath, safeName(cert.Domain.Main), info.Name+info.Ext)
if err := os.MkdirAll(filepath.Join(dumpPath, safeName(cert.Domain.Main)), 0o755); err != nil {
return err
}
}
return os.WriteFile(keyPath, cert.Key, 0o600)
}
func extractPEMPrivateKey(account *traefikv3.Account) []byte {
var block *pem.Block
switch account.KeyType {
case certcrypto.RSA2048, certcrypto.RSA4096, certcrypto.RSA8192:
block = &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: account.PrivateKey,
}
case certcrypto.EC256, certcrypto.EC384:
block = &pem.Block{
Type: "EC PRIVATE KEY",
Bytes: account.PrivateKey,
}
default:
panic(fmt.Sprintf("unsupported key type: '%v'", account.KeyType))
}
return pem.EncodeToMemory(block)
}
func cleanDir(dumpPath string) error {
_, errExists := os.Stat(dumpPath)
if os.IsNotExist(errExists) {
return nil
}
if errExists != nil {
return errExists
}
dir, err := os.ReadDir(dumpPath)
if err != nil {
return err
}
for _, f := range dir {
if err := os.RemoveAll(filepath.Join(dumpPath, f.Name())); err != nil {
return err
}
}
return nil
}

View File

@ -1,8 +0,0 @@
//go:build !windows
// +build !windows
package v3
func safeName(filename string) string {
return filename
}

View File

@ -1,10 +0,0 @@
//go:build windows
// +build windows
package v3
import "strings"
func safeName(filename string) string {
return strings.ReplaceAll(filename, "*", "_")
}

95
go.mod
View File

@ -1,90 +1,11 @@
module github.com/ldez/traefik-certs-dumper/v2 module github.com/ldez/traefik-certs-dumper
go 1.24.0
require ( require (
github.com/charmbracelet/lipgloss v1.0.0 github.com/cenkalti/backoff v2.1.1+incompatible // indirect
github.com/fsnotify/fsnotify v1.9.0 github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/go-acme/lego/v4 v4.25.2 github.com/spf13/cobra v0.0.3
github.com/kvtools/boltdb v1.0.2 github.com/spf13/pflag v1.0.3 // indirect
github.com/kvtools/consul v1.0.2 github.com/xenolf/lego v2.2.0+incompatible
github.com/kvtools/etcdv2 v1.0.2 golang.org/x/crypto v0.0.0-20190208162236-193df9c0f06f // indirect
github.com/kvtools/etcdv3 v1.0.2 gopkg.in/square/go-jose.v2 v2.2.2 // indirect
github.com/kvtools/valkeyrie v1.0.0
github.com/kvtools/zookeeper v1.0.2
github.com/mitchellh/go-homedir v1.1.0
github.com/spf13/cobra v1.9.1
github.com/spf13/viper v1.19.0
github.com/stretchr/testify v1.10.0
) )
require (
github.com/armon/go-metrics v0.4.1 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/charmbracelet/x/ansi v0.4.2 // indirect
github.com/coreos/go-semver v0.3.1 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/go-jose/go-jose/v4 v4.1.1 // indirect
github.com/go-zookeeper/zk v1.0.4 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/hashicorp/consul/api v1.28.2 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-hclog v1.6.3 // indirect
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
github.com/hashicorp/go-metrics v0.5.4 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
github.com/hashicorp/golang-lru v1.0.2 // indirect
github.com/hashicorp/hcl v1.0.1-vault-5 // indirect
github.com/hashicorp/serf v0.10.2 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/magiconair/properties v1.8.9 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mitchellh/mapstructure v1.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/muesli/termenv v0.15.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.7.1 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
go.etcd.io/bbolt v1.3.6 // indirect
go.etcd.io/etcd/api/v3 v3.5.14 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.5.14 // indirect
go.etcd.io/etcd/client/v2 v2.305.12 // indirect
go.etcd.io/etcd/client/v3 v3.5.14 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/crypto v0.40.0 // indirect
golang.org/x/exp v0.0.0-20241210194714-1829a127f884 // indirect
golang.org/x/net v0.42.0 // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/text v0.27.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect
google.golang.org/grpc v1.73.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
exclude github.com/tencentcloud/tencentcloud-sdk-go v3.0.83+incompatible

406
go.sum
View File

@ -1,393 +1,21 @@
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/cenkalti/backoff v2.1.1+incompatible h1:tKJnvO2kl0zmb/jA5UKAt4VoEVw1qxKWjE/Bpp46npY=
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA=
github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
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.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg=
github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo=
github.com/charmbracelet/x/ansi v0.4.2 h1:0JM6Aj/g/KC154/gOP4vfxun0ff6itogDYk41kof+qk=
github.com/charmbracelet/x/ansi v0.4.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
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/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.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-acme/lego/v4 v4.25.2 h1:+D1Q+VnZrD+WJdlkgUEGHFFTcDrwGlE7q24IFtMmHDI=
github.com/go-acme/lego/v4 v4.25.2/go.mod h1:OORYyVNZPaNdIdVYCGSBNRNZDIjhQbPuFxwGDgWj/yM=
github.com/go-jose/go-jose/v4 v4.1.1 h1:JYhSgy4mXXzAdF3nUx3ygx347LRXJRrpgyU3adRmkAI=
github.com/go-jose/go-jose/v4 v4.1.1/go.mod h1:BdsZGqgdO3b6tTc6LSE56wcDbMMLuPsw5d4ZD5f94kA=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
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-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-zookeeper/zk v1.0.4 h1:DPzxraQx7OrPyXq2phlGlNSIyWEsAox0RJmjTseMV6I=
github.com/go-zookeeper/zk v1.0.4/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
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/hashicorp/consul/api v1.28.2 h1:mXfkRHrpHN4YY3RqL09nXU1eHKLNiuAN4kHvDQ16k/8=
github.com/hashicorp/consul/api v1.28.2/go.mod h1:KyzqzgMEya+IZPcD65YFoOVAgPpbfERu4I/tzG6/ueE=
github.com/hashicorp/consul/sdk v0.16.0 h1:SE9m0W6DEfgIVCJX7xU+iv/hUl4m/nxqMTnCdMxDpJ8=
github.com/hashicorp/consul/sdk v0.16.0/go.mod h1:7pxqqhqoaPqnBnzXD1StKed62LqJeClzVsUEy85Zr0A=
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.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc=
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-metrics v0.5.4 h1:8mmPiIJkTPPEbAiV97IxdAGNdRdaWwVap1BU6elejKY=
github.com/hashicorp/go-metrics v0.5.4/go.mod h1:CG5yz4NZ/AI/aQt9Ucm/vdBnbh7fvmv4lxZ350i+QQI=
github.com/hashicorp/go-msgpack v0.5.5 h1:i9R9JSrqIz0QVLz3sz+i3YJdT7TTSLcfLLzJi9aZTuI=
github.com/hashicorp/go-msgpack/v2 v2.1.2 h1:4Ee8FTp834e+ewB71RDrQ0VKpyFdrKOjvYtnQ/ltVj0=
github.com/hashicorp/go-msgpack/v2 v2.1.2/go.mod h1:upybraOAblm4S7rx0+jeNy+CWWhzywQsSRV5033mMu4=
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.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
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-sockaddr v1.0.5 h1:dvk7TIXCZpmfOlM+9mlcrWmWjw/wlKT+VDq2wMvfPJU=
github.com/hashicorp/go-sockaddr v1.0.5/go.mod h1:uoUUmtwU7n9Dv3O4SNLeFvg0SxQ3lyjsj6+CCykpaxI=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/hcl v1.0.1-vault-5 h1:kI3hhbbyzr4dldA8UdTb7ZlVVlI2DACdCfz31RPDgJM=
github.com/hashicorp/hcl v1.0.1-vault-5/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM=
github.com/hashicorp/memberlist v0.5.2 h1:rJoNPWZ0juJBgqn48gjy59K5H4rNgvUoM1kUD7bXiuI=
github.com/hashicorp/memberlist v0.5.2/go.mod h1:Ri9p/tRShbjYnpNf4FFPXG7wxEGY4Nrcn6E7jrVa//4=
github.com/hashicorp/serf v0.10.2 h1:m5IORhuNSjaxeljg5DeQVDlQyVkhRIjJDimbkCa8aAc=
github.com/hashicorp/serf v0.10.2/go.mod h1:T1CmSGfSeGfnfNy/w0odXQUR1rfECGd2Qdsp84DjOiY=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.11/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/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
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/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.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/kvtools/boltdb v1.0.2 h1:GZaUZ3AOw33mdK8r7p5bRqfhe504/BcwSVj6efQzLNs=
github.com/kvtools/boltdb v1.0.2/go.mod h1:ERqvkjiHCFTLy7WcHitvgpDH0y14pT3r+ZwkfqJ6tQM=
github.com/kvtools/consul v1.0.2 h1:ltPgs4Ld09Xaa7zrOJ/TewBYKAsr11/LRFpErdkb8AA=
github.com/kvtools/consul v1.0.2/go.mod h1:bFnzfGJ5ZIRRXCBGBmwhJlLdEWOlrjOcS1WjyAQzaJA=
github.com/kvtools/etcdv2 v1.0.2 h1:Bigfbm2f4uZveD0Av/5Vxs7MdqnWDVnAFpSF4QEL1c8=
github.com/kvtools/etcdv2 v1.0.2/go.mod h1:Ye5IwvG5KxUdcP14Yag6Pc3E9bzxy1j7zlvLK8eI/bU=
github.com/kvtools/etcdv3 v1.0.2 h1:EB0mAtzqe1folE7m7Q6wnCXcGwaOmrYmsVmF3hNsTKI=
github.com/kvtools/etcdv3 v1.0.2/go.mod h1:Xr6DbwqjuCEcXAIWmXxw0DX+N5BhuvablXgN90XeqMM=
github.com/kvtools/valkeyrie v1.0.0 h1:LAITop2wPoYCMitR24GZZsW0b57hmI+ePD18VRTtOf0=
github.com/kvtools/valkeyrie v1.0.0/go.mod h1:bDi/OdhJCSbGPMsCgUQl881yuEweKCSItAtTBI+ZjpU=
github.com/kvtools/zookeeper v1.0.2 h1:uK0CzQa+mtKGxDDH+DeqXo2HC1Kx4hWXZ7pX/zS4aTo=
github.com/kvtools/zookeeper v1.0.2/go.mod h1:6TfxUwJ7IuBk5srgnoe528W0ftanNECHgOiShx/t0Aw=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
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.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.1.67 h1:kg0EHj0G4bfT5/oOys6HhZw4vmMlnoZ+gDu8tJ/AlI0=
github.com/miekg/dns v1.1.67/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
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/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/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
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.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
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/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
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.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/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.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/xenolf/lego v2.2.0+incompatible h1:r4UAcpgPmX3j0aThoVrRM1FFLcvyy08UyGbIwFU4zoQ=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/xenolf/lego v2.2.0+incompatible/go.mod h1:fwiGnfsIjG7OHPfOvgK7Y/Qo6+2Ox0iozjNTkZICKbY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= golang.org/x/crypto v0.0.0-20190208162236-193df9c0f06f h1:ETU2VEl7TnT5bl7IvuKEzTDpplg5wzGYsOCAPhdoEIg=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= golang.org/x/crypto v0.0.0-20190208162236-193df9c0f06f/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= gopkg.in/square/go-jose.v2 v2.2.2 h1:orlkJ3myw8CN1nVQHBFfloD+L3egixIa4FvUP6RosSA=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
go.etcd.io/etcd/api/v3 v3.5.14 h1:vHObSCxyB9zlF60w7qzAdTcGaglbJOpSj1Xj9+WGxq0=
go.etcd.io/etcd/api/v3 v3.5.14/go.mod h1:BmtWcRlQvwa1h3G2jvKYwIQy4PkHlDej5t7uLMUdJUU=
go.etcd.io/etcd/client/pkg/v3 v3.5.14 h1:SaNH6Y+rVEdxfpA2Jr5wkEvN6Zykme5+YnbCkxvuWxQ=
go.etcd.io/etcd/client/pkg/v3 v3.5.14/go.mod h1:8uMgAokyG1czCtIdsq+AGyYQMvpIKnSvPjFMunkgeZI=
go.etcd.io/etcd/client/v2 v2.305.12 h1:0m4ovXYo1CHaA/Mp3X/Fak5sRNIWf01wk/X1/G3sGKI=
go.etcd.io/etcd/client/v2 v2.305.12/go.mod h1:aQ/yhsxMu+Oht1FOupSr60oBvcS9cKXHrzBpDsPTf9E=
go.etcd.io/etcd/client/v3 v3.5.14 h1:CWfRs4FDaDoSz81giL7zPpZH2Z35tbOrAJkkjMqOupg=
go.etcd.io/etcd/client/v3 v3.5.14/go.mod h1:k3XfdV/VIHy/97rqWjoUzrj9tk7GgJGH9J8L4dNXmAk=
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/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/exp v0.0.0-20241210194714-1829a127f884 h1:Y/Mj/94zIQQGHVSv1tTtQBDaQaJe62U9bkDZKKyhPCU=
golang.org/x/exp v0.0.0-20241210194714-1829a127f884/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/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-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-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
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.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
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/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 h1:FiusG7LWj+4byqhbvmB+Q93B/mOxJLN2DTozDuZm4EU=
google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:kXqgZtrWaf6qS3jZOCnCH7WYfrvFjkC51bM8fz3RsCA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 h1:pFyd6EwwL2TqFf8emdthzeX+gZE1ElRq3iM8pui4KBY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-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/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -1,420 +0,0 @@
#!/bin/sh
set -e
# Code generated by godownloader on 2019-04-04T19:26:29Z. DO NOT EDIT.
#
usage() {
this=$1
cat <<EOF
$this: download go binaries for ldez/traefik-certs-dumper
Usage: $this [-b] bindir [-d] [tag]
-b sets bindir or installation directory, Defaults to ./bin
-d turns on debug logging
[tag] is a tag from
https://github.com/ldez/traefik-certs-dumper/releases
If tag is missing, then the latest will be used.
Generated by godownloader
https://github.com/goreleaser/godownloader
EOF
exit 2
}
parse_args() {
#BINDIR is ./bin unless set be ENV
# over-ridden by flag below
BINDIR=${BINDIR:-./bin}
while getopts "b:dh?x" arg; do
case "$arg" in
b) BINDIR="$OPTARG" ;;
d) log_set_priority 10 ;;
h | \?) usage "$0" ;;
x) set -x ;;
esac
done
shift $((OPTIND - 1))
TAG=$1
}
# this function wraps all the destructive operations
# if a curl|bash cuts off the end of the script due to
# network, either nothing will happen or will syntax error
# out preventing half-done work
execute() {
tmpdir=$(mktmpdir)
log_debug "downloading files into ${tmpdir}"
http_download "${tmpdir}/${TARBALL}" "${TARBALL_URL}"
http_download "${tmpdir}/${CHECKSUM}" "${CHECKSUM_URL}"
hash_sha256_verify "${tmpdir}/${TARBALL}" "${tmpdir}/${CHECKSUM}"
srcdir="${tmpdir}"
(cd "${tmpdir}" && untar "${TARBALL}")
install -d "${BINDIR}"
for binexe in "traefik-certs-dumper" ; do
if [ "$OS" = "windows" ]; then
binexe="${binexe}.exe"
fi
install "${srcdir}/${binexe}" "${BINDIR}/"
log_info "installed ${BINDIR}/${binexe}"
done
}
is_supported_platform() {
platform=$1
found=1
case "$platform" in
windows/amd64) found=0 ;;
windows/386) found=0 ;;
windows/arm64) found=0 ;;
darwin/amd64) found=0 ;;
darwin/386) found=0 ;;
darwin/arm64) found=0 ;;
linux/amd64) found=0 ;;
linux/386) found=0 ;;
linux/arm64) found=0 ;;
freebsd/amd64) found=0 ;;
freebsd/386) found=0 ;;
freebsd/arm64) found=0 ;;
openbsd/amd64) found=0 ;;
openbsd/386) found=0 ;;
openbsd/arm64) found=0 ;;
windows/armv7) found=0 ;;
windows/armv6) found=0 ;;
windows/armv5) found=0 ;;
darwin/armv7) found=0 ;;
darwin/armv6) found=0 ;;
darwin/armv5) found=0 ;;
linux/armv7) found=0 ;;
linux/armv6) found=0 ;;
linux/armv5) found=0 ;;
freebsd/armv7) found=0 ;;
freebsd/armv6) found=0 ;;
freebsd/armv5) found=0 ;;
openbsd/armv7) found=0 ;;
openbsd/armv6) found=0 ;;
openbsd/armv5) found=0 ;;
esac
case "$platform" in
darwin/386) found=1 ;;
esac
return $found
}
check_platform() {
if is_supported_platform "$PLATFORM"; then
# optional logging goes here
true
else
log_crit "platform $PLATFORM is not supported. Make sure this script is up-to-date and file request at https://github.com/${PREFIX}/issues/new"
exit 1
fi
}
tag_to_version() {
if [ -z "${TAG}" ]; then
log_info "checking GitHub for latest tag"
else
log_info "checking GitHub for tag '${TAG}'"
fi
REALTAG=$(github_release "$OWNER/$REPO" "${TAG}") && true
if test -z "$REALTAG"; then
log_crit "unable to find '${TAG}' - use 'latest' or see https://github.com/${PREFIX}/releases for details"
exit 1
fi
# if version starts with 'v', remove it
TAG="$REALTAG"
VERSION=${TAG#v}
}
adjust_format() {
# change format (tar.gz or zip) based on ARCH
case ${ARCH} in
windows) FORMAT=zip ;;
esac
true
}
adjust_os() {
# adjust archive name based on OS
true
}
adjust_arch() {
# adjust archive name based on ARCH
true
}
cat /dev/null <<EOF
------------------------------------------------------------------------
https://github.com/client9/shlib - portable posix shell functions
Public domain - http://unlicense.org
https://github.com/client9/shlib/blob/master/LICENSE.md
but credit (and pull requests) appreciated.
------------------------------------------------------------------------
EOF
is_command() {
command -v "$1" >/dev/null
}
echoerr() {
echo "$@" 1>&2
}
log_prefix() {
echo "$0"
}
_logp=6
log_set_priority() {
_logp="$1"
}
log_priority() {
if test -z "$1"; then
echo "$_logp"
return
fi
[ "$1" -le "$_logp" ]
}
log_tag() {
case $1 in
0) echo "emerg" ;;
1) echo "alert" ;;
2) echo "crit" ;;
3) echo "err" ;;
4) echo "warning" ;;
5) echo "notice" ;;
6) echo "info" ;;
7) echo "debug" ;;
*) echo "$1" ;;
esac
}
log_debug() {
log_priority 7 || return 0
echoerr "$(log_prefix)" "$(log_tag 7)" "$@"
}
log_info() {
log_priority 6 || return 0
echoerr "$(log_prefix)" "$(log_tag 6)" "$@"
}
log_err() {
log_priority 3 || return 0
echoerr "$(log_prefix)" "$(log_tag 3)" "$@"
}
log_crit() {
log_priority 2 || return 0
echoerr "$(log_prefix)" "$(log_tag 2)" "$@"
}
uname_os() {
os=$(uname -s | tr '[:upper:]' '[:lower:]')
case "$os" in
msys_nt) os="windows" ;;
esac
echo "$os"
}
uname_arch() {
arch=$(uname -m)
case $arch in
x86_64) arch="amd64" ;;
x86) arch="386" ;;
i686) arch="386" ;;
i386) arch="386" ;;
aarch64) arch="arm64" ;;
armv5*) arch="armv5" ;;
armv6*) arch="armv6" ;;
armv7*) arch="armv7" ;;
esac
echo ${arch}
}
uname_os_check() {
os=$(uname_os)
case "$os" in
darwin) return 0 ;;
dragonfly) return 0 ;;
freebsd) return 0 ;;
linux) return 0 ;;
android) return 0 ;;
nacl) return 0 ;;
netbsd) return 0 ;;
openbsd) return 0 ;;
plan9) return 0 ;;
solaris) return 0 ;;
windows) return 0 ;;
esac
log_crit "uname_os_check '$(uname -s)' got converted to '$os' which is not a GOOS value. Please file bug at https://github.com/client9/shlib"
return 1
}
uname_arch_check() {
arch=$(uname_arch)
case "$arch" in
386) return 0 ;;
amd64) return 0 ;;
arm64) return 0 ;;
armv5) return 0 ;;
armv6) return 0 ;;
armv7) return 0 ;;
ppc64) return 0 ;;
ppc64le) return 0 ;;
mips) return 0 ;;
mipsle) return 0 ;;
mips64) return 0 ;;
mips64le) return 0 ;;
s390x) return 0 ;;
amd64p32) return 0 ;;
esac
log_crit "uname_arch_check '$(uname -m)' got converted to '$arch' which is not a GOARCH value. Please file bug report at https://github.com/client9/shlib"
return 1
}
untar() {
tarball=$1
case "${tarball}" in
*.tar.gz | *.tgz) tar -xzf "${tarball}" ;;
*.tar) tar -xf "${tarball}" ;;
*.zip) unzip "${tarball}" ;;
*)
log_err "untar unknown archive format for ${tarball}"
return 1
;;
esac
}
mktmpdir() {
test -z "$TMPDIR" && TMPDIR="$(mktemp -d)"
mkdir -p "${TMPDIR}"
echo "${TMPDIR}"
}
http_download_curl() {
local_file=$1
source_url=$2
header=$3
if [ -z "$header" ]; then
code=$(curl -w '%{http_code}' -sL -o "$local_file" "$source_url")
else
code=$(curl -w '%{http_code}' -sL -H "$header" -o "$local_file" "$source_url")
fi
if [ "$code" != "200" ]; then
log_debug "http_download_curl received HTTP status $code"
return 1
fi
return 0
}
http_download_wget() {
local_file=$1
source_url=$2
header=$3
if [ -z "$header" ]; then
wget -q -O "$local_file" "$source_url"
else
wget -q --header "$header" -O "$local_file" "$source_url"
fi
}
http_download() {
log_debug "http_download $2"
if is_command curl; then
http_download_curl "$@"
return
elif is_command wget; then
http_download_wget "$@"
return
fi
log_crit "http_download unable to find wget or curl"
return 1
}
http_copy() {
tmp=$(mktemp)
http_download "${tmp}" "$1" "$2" || return 1
body=$(cat "$tmp")
rm -f "${tmp}"
echo "$body"
}
github_release() {
owner_repo=$1
version=$2
test -z "$version" && version="latest"
giturl="https://github.com/${owner_repo}/releases/${version}"
json=$(http_copy "$giturl" "Accept:application/json")
test -z "$json" && return 1
version=$(echo "$json" | tr -s '\n' ' ' | sed 's/.*"tag_name":"//' | sed 's/".*//')
test -z "$version" && return 1
echo "$version"
}
hash_sha256() {
TARGET=${1:-/dev/stdin}
if is_command gsha256sum; then
hash=$(gsha256sum "$TARGET") || return 1
echo "$hash" | cut -d ' ' -f 1
elif is_command sha256sum; then
hash=$(sha256sum "$TARGET") || return 1
echo "$hash" | cut -d ' ' -f 1
elif is_command shasum; then
hash=$(shasum -a 256 "$TARGET" 2>/dev/null) || return 1
echo "$hash" | cut -d ' ' -f 1
elif is_command openssl; then
hash=$(openssl -dst openssl dgst -sha256 "$TARGET") || return 1
echo "$hash" | cut -d ' ' -f a
else
log_crit "hash_sha256 unable to find command to compute sha-256 hash"
return 1
fi
}
hash_sha256_verify() {
TARGET=$1
checksums=$2
if [ -z "$checksums" ]; then
log_err "hash_sha256_verify checksum file not specified in arg2"
return 1
fi
BASENAME=${TARGET##*/}
want=$(grep "${BASENAME}" "${checksums}" 2>/dev/null | tr '\t' ' ' | cut -d ' ' -f 1)
if [ -z "$want" ]; then
log_err "hash_sha256_verify unable to find checksum for '${TARGET}' in '${checksums}'"
return 1
fi
got=$(hash_sha256 "$TARGET")
if [ "$want" != "$got" ]; then
log_err "hash_sha256_verify checksum for '$TARGET' did not verify ${want} vs $got"
return 1
fi
}
cat /dev/null <<EOF
------------------------------------------------------------------------
End of functions from https://github.com/client9/shlib
------------------------------------------------------------------------
EOF
PROJECT_NAME="traefik-certs-dumper"
OWNER=ldez
REPO="traefik-certs-dumper"
BINARY=traefik-certs-dumper
FORMAT=tar.gz
OS=$(uname_os)
ARCH=$(uname_arch)
PREFIX="$OWNER/$REPO"
# use in logging routines
log_prefix() {
echo "$PREFIX"
}
PLATFORM="${OS}/${ARCH}"
GITHUB_DOWNLOAD=https://github.com/${OWNER}/${REPO}/releases/download
uname_os_check "$OS"
uname_arch_check "$ARCH"
parse_args "$@"
check_platform
tag_to_version
adjust_format
adjust_os
adjust_arch
log_info "found version: ${VERSION} for ${TAG}/${OS}/${ARCH}"
NAME=${PROJECT_NAME}_v${VERSION}_${OS}_${ARCH}
TARBALL=${NAME}.${FORMAT}
TARBALL_URL=${GITHUB_DOWNLOAD}/${TAG}/${TARBALL}
CHECKSUM=${PROJECT_NAME}_${VERSION}_checksums.txt
CHECKSUM_URL=${GITHUB_DOWNLOAD}/${TAG}/${CHECKSUM}
execute

View File

@ -1,42 +0,0 @@
package hook
import (
"context"
"errors"
"fmt"
"os"
"os/exec"
"strings"
"time"
)
// Exec Execute a command on a go routine.
func Exec(ctx context.Context, command string) {
if command == "" {
return
}
go func() {
errH := execute(ctx, command)
if errH != nil {
panic(errH)
}
}()
}
func execute(ctx context.Context, command string) error {
ctxCmd, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
parts := strings.Fields(os.ExpandEnv(command))
output, err := exec.CommandContext(ctxCmd, parts[0], parts[1:]...).CombinedOutput()
if len(output) > 0 {
fmt.Println(string(output))
}
if errors.Is(ctxCmd.Err(), context.DeadlineExceeded) {
return errors.New("hook timed out")
}
return err
}

View File

@ -1,30 +0,0 @@
package hook
import (
"testing"
)
func Test_execute(t *testing.T) {
testCases := []struct {
desc string
command string
}{
{
desc: "expand env vars",
command: `echo "${GOPATH} ${GOARCH}"`,
},
{
desc: "simple",
command: `echo 'hello'`,
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
err := execute(t.Context(), test.command)
if err != nil {
t.Fatal(err)
}
})
}
}

View File

@ -1,18 +0,0 @@
version: '3'
services:
consul-kv:
image: consul
ports:
- "8500:8500"
zookeeper-kv:
image: zookeeper
ports:
- "2181:2181"
etcd-kv:
image: quay.io/coreos/etcd:v3.3.12
command: etcd --listen-client-urls http://0.0.0.0:2379 --advertise-client-urls http://0.0.0.0:2380
ports:
- "2379:2379"

View File

@ -1,111 +0,0 @@
package main
import (
"bytes"
"compress/gzip"
"context"
"log"
"os"
"path/filepath"
"time"
"github.com/kvtools/boltdb"
"github.com/kvtools/consul"
"github.com/kvtools/etcdv3"
"github.com/kvtools/valkeyrie"
"github.com/kvtools/zookeeper"
)
const storeKey = "traefik/acme/account/object"
func main() {
log.SetFlags(log.Lshortfile)
source := "./acme.json"
err := loadData(context.Background(), source)
if err != nil {
log.Fatal(err)
}
}
func loadData(ctx context.Context, source string) error {
content, err := readFile(source)
if err != nil {
return err
}
// Consul
err = putData(ctx, consul.StoreName, []string{"localhost:8500"},
&consul.Config{ConnectionTimeout: 3 * time.Second}, content)
if err != nil {
return err
}
// ETCD v3
err = putData(ctx, etcdv3.StoreName, []string{"localhost:2379"},
&etcdv3.Config{ConnectionTimeout: 3 * time.Second}, content)
if err != nil {
return err
}
// Zookeeper
err = putData(ctx, zookeeper.StoreName, []string{"localhost:2181"},
&zookeeper.Config{ConnectionTimeout: 3 * time.Second}, content)
if err != nil {
return err
}
// BoltDB
err = putData(ctx, boltdb.StoreName, []string{"/tmp/test-traefik-certs-dumper.db"},
&boltdb.Config{ConnectionTimeout: 3 * time.Second, Bucket: "traefik"}, content)
if err != nil {
return err
}
return nil
}
func putData(ctx context.Context, backend string, addrs []string, options valkeyrie.Config, content []byte) error {
kvStore, err := valkeyrie.NewStore(ctx, backend, addrs, options)
if err != nil {
return err
}
if err := kvStore.Put(ctx, storeKey, content, nil); err != nil {
return err
}
log.Printf("Successfully updated %s.\n", backend)
return nil
}
func readFile(source string) ([]byte, error) {
content, err := os.ReadFile(filepath.Clean(source))
if err != nil {
return nil, err
}
var b bytes.Buffer
gz := gzip.NewWriter(&b)
defer func() {
if errC := gz.Close(); errC != nil {
log.Println(errC)
}
}()
if _, err = gz.Write(content); err != nil {
return nil, err
}
if err = gz.Flush(); err != nil {
return nil, err
}
if err := gz.Close(); err != nil {
return nil, err
}
return b.Bytes(), nil
}

View File

@ -1,54 +0,0 @@
# Integration testing
## Preparation
- Create valid ACME file `./acme.json`
- Start backends using docker
```bash
docker-compose -f integrationtest/docker-compose.yml up
```
- Initialize backends
```bash
go run integrationtest/loader.go
```
## Run certs dumper without watching
```bash
traefik-certs-dumper file
# http://localhost:8500/ui/
traefik-certs-dumper kv consul --endpoints localhost:8500
traefik-certs-dumper kv etcd --endpoints localhost:2379
traefik-certs-dumper kv boltdb --endpoints /tmp/test-traefik-certs-dumper.db
traefik-certs-dumper kv zookeeper --endpoints localhost:2181
```
## Run certs dumper with watching
While watching is enabled, manipulate `./acme.json` for file backend or run `loader.go` again for KV backends so that change events are triggered.
```bash
traefik-certs-dumper file --watch
traefik-certs-dumper kv consul --watch --endpoints localhost:8500
traefik-certs-dumper kv etcd --watch --endpoints localhost:2379
traefik-certs-dumper kv zookeeper --watch --endpoints localhost:2181
```
## Cleanup
- Stop backends
```bash
docker-compose -f integrationtest/docker-compose.yml down
```

View File

@ -1,35 +0,0 @@
package traefikv1
import (
"github.com/go-acme/lego/v4/certcrypto"
"github.com/go-acme/lego/v4/registration"
)
// StoredData represents the data managed by the Store.
type StoredData struct {
Account *Account
Certificates []*Certificate
HTTPChallenges map[string]map[string][]byte
TLSChallenges map[string]*Certificate
}
// Certificate is a struct which contains all data needed from an ACME certificate.
type Certificate struct {
Domain Domain
Certificate []byte
Key []byte
}
// Domain holds a domain name with SANs.
type Domain struct {
Main string
SANs []string
}
// Account is used to store lets encrypt registration info.
type Account struct {
Email string
Registration *registration.Resource
PrivateKey []byte
KeyType certcrypto.KeyType
}

View File

@ -1,101 +0,0 @@
package traefikv2
import (
"crypto"
"crypto/x509"
"github.com/go-acme/lego/v4/certcrypto"
"github.com/go-acme/lego/v4/registration"
)
// StoredData represents the data managed by Store.
type StoredData struct {
Account *Account
Certificates []*CertAndStore
}
// Account is used to store lets encrypt registration info.
type Account struct {
Email string
Registration *registration.Resource
PrivateKey []byte
KeyType certcrypto.KeyType
}
// GetEmail returns email.
func (a *Account) GetEmail() string {
return a.Email
}
// GetRegistration returns lets encrypt registration resource.
func (a *Account) GetRegistration() *registration.Resource {
return a.Registration
}
// GetPrivateKey returns private key.
func (a *Account) GetPrivateKey() crypto.PrivateKey {
privateKey, err := x509.ParsePKCS1PrivateKey(a.PrivateKey)
if err != nil {
return nil
}
return privateKey
}
// CertAndStore allows mapping a TLS certificate to a TLS store.
type CertAndStore struct {
Certificate
Store string
}
// Certificate is a struct which contains all data needed from an ACME certificate.
type Certificate struct {
Domain Domain `json:"domain,omitempty" toml:"domain,omitempty" yaml:"domain,omitempty"`
Certificate []byte `json:"certificate,omitempty" toml:"certificate,omitempty" yaml:"certificate,omitempty"`
Key []byte `json:"key,omitempty" toml:"key,omitempty" yaml:"key,omitempty"`
}
// Domain holds a domain name with SANs.
type Domain struct {
// Main defines the main domain name.
Main string `description:"Default subject name." json:"main,omitempty" toml:"main,omitempty" yaml:"main,omitempty"`
// SANs defines the subject alternative domain names.
SANs []string `description:"Subject alternative names." json:"sans,omitempty" toml:"sans,omitempty" yaml:"sans,omitempty"`
}
// ToStrArray convert a domain into an array of strings.
func (d *Domain) ToStrArray() []string {
var domains []string
if d.Main != "" {
domains = []string{d.Main}
}
return append(domains, d.SANs...)
}
// Set sets a domains from an array of strings.
func (d *Domain) Set(domains []string) {
if len(domains) > 0 {
d.Main = domains[0]
d.SANs = domains[1:]
}
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (d *Domain) DeepCopyInto(out *Domain) {
*out = *d
if d.SANs != nil {
in, out := &d.SANs, &out.SANs
*out = make([]string, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Domain.
func (d *Domain) DeepCopy() *Domain {
if d == nil {
return nil
}
out := new(Domain)
d.DeepCopyInto(out)
return out
}

View File

@ -1,101 +0,0 @@
package traefikv3
import (
"crypto"
"crypto/x509"
"github.com/go-acme/lego/v4/certcrypto"
"github.com/go-acme/lego/v4/registration"
)
// StoredData represents the data managed by Store.
type StoredData struct {
Account *Account
Certificates []*CertAndStore
}
// Account is used to store lets encrypt registration info.
type Account struct {
Email string
Registration *registration.Resource
PrivateKey []byte
KeyType certcrypto.KeyType
}
// GetEmail returns email.
func (a *Account) GetEmail() string {
return a.Email
}
// GetRegistration returns lets encrypt registration resource.
func (a *Account) GetRegistration() *registration.Resource {
return a.Registration
}
// GetPrivateKey returns private key.
func (a *Account) GetPrivateKey() crypto.PrivateKey {
privateKey, err := x509.ParsePKCS1PrivateKey(a.PrivateKey)
if err != nil {
return nil
}
return privateKey
}
// CertAndStore allows mapping a TLS certificate to a TLS store.
type CertAndStore struct {
Certificate
Store string
}
// Certificate is a struct which contains all data needed from an ACME certificate.
type Certificate struct {
Domain Domain `json:"domain,omitempty" toml:"domain,omitempty" yaml:"domain,omitempty"`
Certificate []byte `json:"certificate,omitempty" toml:"certificate,omitempty" yaml:"certificate,omitempty"`
Key []byte `json:"key,omitempty" toml:"key,omitempty" yaml:"key,omitempty"`
}
// Domain holds a domain name with SANs.
type Domain struct {
// Main defines the main domain name.
Main string `description:"Default subject name." json:"main,omitempty" toml:"main,omitempty" yaml:"main,omitempty"`
// SANs defines the subject alternative domain names.
SANs []string `description:"Subject alternative names." json:"sans,omitempty" toml:"sans,omitempty" yaml:"sans,omitempty"`
}
// ToStrArray convert a domain into an array of strings.
func (d *Domain) ToStrArray() []string {
var domains []string
if d.Main != "" {
domains = []string{d.Main}
}
return append(domains, d.SANs...)
}
// Set sets a domains from an array of strings.
func (d *Domain) Set(domains []string) {
if len(domains) > 0 {
d.Main = domains[0]
d.SANs = domains[1:]
}
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (d *Domain) DeepCopyInto(out *Domain) {
*out = *d
if d.SANs != nil {
in, out := &d.SANs, &out.SANs
*out = make([]string, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Domain.
func (d *Domain) DeepCopy() *Domain {
if d == nil {
return nil
}
out := new(Domain)
d.DeepCopyInto(out)
return out
}

53
main.go
View File

@ -1,12 +1,59 @@
package main package main
import ( import (
"fmt"
"log" "log"
"os"
"strconv"
"github.com/ldez/traefik-certs-dumper/v2/cmd" "github.com/spf13/cobra"
) )
func main() { func main() {
log.SetFlags(log.LstdFlags | log.Lshortfile) var rootCmd = &cobra.Command{
cmd.Execute() Use: "traefik-certs-dumper",
Short: "Dump Let's Encrypt certificates from Traefik",
Long: `Dump the content of the "acme.json" file from Traefik to certificates.`,
Version: version,
}
var dumpCmd = &cobra.Command{
Use: "dump",
Short: "Dump Let's Encrypt certificates from Traefik",
Long: `Dump the content of the "acme.json" file from Traefik to certificates.`,
Run: func(cmd *cobra.Command, _ []string) {
acmeFile := cmd.Flag("source").Value.String()
dumpPath := cmd.Flag("dest").Value.String()
crtExt := cmd.Flag("crt-ext").Value.String()
keyExt := cmd.Flag("key-ext").Value.String()
subDir, _ := strconv.ParseBool(cmd.Flag("domain-subdir").Value.String())
err := dump(acmeFile, dumpPath, crtExt, keyExt, subDir)
if err != nil {
log.Fatal(err)
}
},
}
dumpCmd.Flags().String("source", "./acme.json", "Path to 'acme.json' file.")
dumpCmd.Flags().String("dest", "./dump", "Path to store the dump content.")
dumpCmd.Flags().String("crt-ext", ".crt", "The file extension of the generated certificates.")
dumpCmd.Flags().String("key-ext", ".key", "The file extension of the generated private keys.")
dumpCmd.Flags().Bool("domain-subdir", false, "Use domain as sub-directory.")
rootCmd.AddCommand(dumpCmd)
var versionCmd = &cobra.Command{
Use: "version",
Short: "Display version",
Run: func(_ *cobra.Command, _ []string) {
displayVersion(rootCmd.Name())
},
}
rootCmd.AddCommand(versionCmd)
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
} }

199
readme.md
View File

@ -1,189 +1,66 @@
# traefik-certs-dumper # traefik-certs-dumper
[![GitHub release](https://img.shields.io/github/release/ldez/traefik-certs-dumper.svg)](https://github.com/ldez/traefik-certs-dumper/releases/latest) [![GitHub release](https://img.shields.io/github/release/ldez/traefik-certs-dumper.svg)](https://github.com/ldez/traefik-certs-dumper/releases/latest)
[![Build Status](https://github.com/ldez/traefik-certs-dumper/workflows/Main/badge.svg?branch=master)](https://github.com/ldez/traefik-certs-dumper/actions) [![Build Status](https://travis-ci.org/ldez/traefik-certs-dumper.svg?branch=master)](https://travis-ci.org/ldez/traefik-certs-dumper)
[![Docker Image Version (latest semver)](https://img.shields.io/docker/v/ldez/traefik-certs-dumper)](https://hub.docker.com/r/ldez/traefik-certs-dumper/)
[![Go Report Card](https://goreportcard.com/badge/github.com/ldez/traefik-certs-dumper)](https://goreportcard.com/report/github.com/ldez/traefik-certs-dumper) [![Go Report Card](https://goreportcard.com/badge/github.com/ldez/traefik-certs-dumper)](https://goreportcard.com/report/github.com/ldez/traefik-certs-dumper)
If you appreciate this project:
[![Sponsor](https://img.shields.io/badge/Sponsor%20me-%E2%9D%A4%EF%B8%8F-pink)](https://github.com/sponsors/ldez) ```yml
Dump the content of the "acme.json" file from Traefik to certificates.
## Features Usage:
traefik-certs-dumper [command]
- Supported sources: Available Commands:
- file ("acme.json") dump Dump Let's Encrypt certificates from Traefik
- KV stores (Consul, Etcd, Zookeeper, Boltdb) help Help about any command
- Watch changes: version Display version
- from file ("acme.json")
- from KV stores (Consul, Etcd, Zookeeper)
- Output formats:
- use domain as subdirectory (allow custom names and extensions)
- flat (domain as filename)
- Hook (only with watch mode and if the data source changes)
- Support Traefik v1, v2, and v3.
## Installation Flags:
-h, --help help for certs-dumper
--version version for certs-dumper
### Download / CI Integration Use "traefik-certs-dumper [command] --help" for more information about a command.
```bash
curl -sfL https://raw.githubusercontent.com/ldez/traefik-certs-dumper/master/godownloader.sh | bash -s -- -b $(go env GOPATH)/bin v2.9.3
``` ```
<!-- ```yml
To generate the script: Dump the content of the "acme.json" file from Traefik to certificates.
```bash Usage:
godownloader --repo=ldez/traefik-certs-dumper -o godownloader.sh traefik-certs-dumper dump [flags]
# or Flags:
--crt-ext string The file extension of the generated certificates. (default ".crt")
godownloader --repo=ldez/traefik-certs-dumper > godownloader.sh --dest string Path to store the dump content. (default "./dump")
--domain-subdir Use domain as sub-directory.
-h, --help help for dump
--key-ext string The file extension of the generated private keys. (default ".key")
--source string Path to 'acme.json' file. (default "./acme.json")
``` ```
-->
### From Binaries
You can use pre-compiled binaries:
* To get the binary just download the latest release for your OS/Arch from [the releases page](https://github.com/ldez/traefik-certs-dumper/releases/)
* Unzip the archive.
* Add `traefik-certs-dumper` in your `PATH`.
### From Docker
```bash
docker run ldez/traefik-certs-dumper:<tag_name>
```
Examples:
- Traefik v1: [docker-compose](docs/docker-compose-traefik-v1.yml)
- Traefik v2: [docker-compose](docs/docker-compose-traefik-v2.yml)
- Traefik v3: TODO
## Usage
- [traefik-certs-dumper](docs/traefik-certs-dumper.md)
- [traefik-certs-dumper file](docs/traefik-certs-dumper_file.md)
- [traefik-certs-dumper kv](docs/traefik-certs-dumper_kv.md)
## Examples ## Examples
### Simple Dump ```bash
traefik-certs-dumper dump
```console
$ traefik-certs-dumper file --version v3
dump
├──certs
│ └──my.domain.com.key
└──private
├──my.domain.com.crt
└──letsencrypt.key
``` ```
### Change source and destination ```bash
traefik-certs-dumper dump --source ./acme.json --dest ./dump
```console
$ traefik-certs-dumper file --version v3 --source ./acme.json --dest ./dump/test
test
├──certs
│ └──my.domain.com.key
└──private
├──my.domain.com.crt
└──letsencrypt.key
``` ```
### Use domain as sub-directory ```bash
traefik-certs-dumper dump --crt-ext=.pem --key-ext=.pem
```console
$ traefik-certs-dumper file --version v3 --domain-subdir=true
dump
├──my.domain.com
│ ├──certificate.crt
│ └──privatekey.key
└──private
└──letsencrypt.key
``` ```
#### Change file extension ```bash
traefik-certs-dumper dump --domain-subdir=true
```console
$ traefik-certs-dumper file --version v3 --domain-subdir --crt-ext=.pem --key-ext=.pem
dump
├──my.domain.com
│ ├──certificate.pem
│ └──privatekey.pem
└──private
└──letsencrypt.key
``` ```
#### Change file name - https://github.com/containous/traefik/issues/4381
- https://github.com/containous/traefik/issues/2418
- https://github.com/containous/traefik/issues/3847
- https://github.com/SvenDowideit/traefik-certdumper
```console ```bash
$ traefik-certs-dumper file --version v3 --domain-subdir --crt-name=fullchain --key-name=privkey traefik-certs-dumper dump --use-subdir=false --crt-ext=.pem --key-ext=.pem --dest="/home/your_user/.homeassistant/"
dump
├──my.domain.com
│ ├──fullchain.crt
│ └──privkey.key
└──private
└──letsencrypt.key
```
## Hook
Hook can be a one-liner passed as a string, or a file for more complex post-hook scenarios.
For the former, create a file (ex: `hook.sh`) and mount it, then pass `sh hooksh` as a parameter to `--post-hook`.
Here is a docker-compose example:
```yml
services:
# ...
traefik-certs-dumper:
image: ldez/traefik-certs-dumper:v2.9.3
container_name: traefik-certs-dumper
entrypoint: sh -c '
while ! [ -e /data/acme.json ]
|| ! [ `jq ".[] | .Certificates | length" /data/acme.json | jq -s "add" ` != 0 ]; do
sleep 1
; done
&& traefik-certs-dumper file --version v2 --watch
--source /data/acme.json --dest /data/certs
--post-hook "sh /hook.sh"'
labels:
traefik.enable: false
volumes:
- ./letsencrypt:/data
- ./hook.sh:/hook.sh
# ...
```
### KV store
#### Consul
```console
$ traefik-certs-dumper kv consul --endpoints localhost:8500
```
#### Etcd
```console
$ traefik-certs-dumper kv etcd --endpoints localhost:2379
```
#### Boltdb
```console
$ traefik-certs-dumper kv boltdb --endpoints /the/path/to/mydb.db
```
#### Zookeeper
```console
$ traefik-certs-dumper kv zookeeper --endpoints localhost:2181
``` ```

View File

@ -1,10 +1,8 @@
package cmd package main
import ( import (
"fmt" "fmt"
"runtime" "runtime"
"github.com/spf13/cobra"
) )
var ( var (
@ -13,19 +11,6 @@ var (
date = "I don't remember exactly" date = "I don't remember exactly"
) )
// versionCmd represents the version command.
var versionCmd = &cobra.Command{
Use: "version",
Short: "Display version",
Run: func(_ *cobra.Command, _ []string) {
displayVersion(rootCmd.Name())
},
}
func init() {
rootCmd.AddCommand(versionCmd)
}
func displayVersion(name string) { func displayVersion(name string) {
fmt.Printf(name+`: fmt.Printf(name+`:
version : %s version : %s