Compare commits

..

No commits in common. "main" and "v2.5.5" have entirely different histories.
main ... v2.5.5

65 changed files with 1264 additions and 2198 deletions

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 }}

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 = "cmd/"
linters = ["gochecknoglobals", "gochecknoinits"]

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,4 +1,3 @@
version: 2
project_name: traefik-certs-dumper
builds:
@ -6,7 +5,7 @@ builds:
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:
- CGO_ENABLED=0
- GO111MODULE=on
goos:
- linux
- darwin
@ -41,182 +40,9 @@ changelog:
archives:
- id: tcd
name_template: '{{ .ProjectName }}_v{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}'
formats: [ 'tar.gz' ]
format: tar.gz
format_overrides:
- goos: windows
formats: [ 'zip' ]
format: zip
files:
- LICENSE
docker_manifests:
- name_template: 'ldez/traefik-certs-dumper:{{ .Tag }}'
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'

64
.travis.yml Normal file
View File

@ -0,0 +1,64 @@
language: go
dist: xenial
notifications:
email:
on_success: never
on_failure: change
cache:
directories:
- $GOPATH/pkg/mod
matrix:
fast_finish: true
include:
- go: 1.13.x
env: STABLE=true
- go: 1.x
- go: tip
allow_failures:
- go: tip
env:
global:
- GO111MODULE=on
services:
- docker
before_install:
# Install linters and misspell
- curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | bash -s -- -b $GOPATH/bin ${GOLANGCI_LINT_VERSION}
- golangci-lint --version
# Install Docker image multi-arch builder
- curl -sfL https://raw.githubusercontent.com/ldez/seihon/master/godownloader.sh | bash -s -- -b "${GOPATH}/bin" ${SEIHON_VERSION}
- seihon --version
install:
- go mod tidy
- git diff --exit-code go.mod
- git diff --exit-code go.sum
- go mod download
before_deploy:
- >
if ! [ "$BEFORE_DEPLOY_RUN" ]; then
export BEFORE_DEPLOY_RUN=1;
echo "${DOCKER_PASSWORD}" | docker login -u "${DOCKER_USERNAME}" --password-stdin
fi
deploy:
- provider: script
skip_cleanup: true
script: curl -sL https://git.io/goreleaser | bash
on:
tags: true
condition: $STABLE = true
- provider: script
skip_cleanup: true
script: make publish-images
on:
tags: true
condition: $STABLE = true

21
Dockerfile Normal file
View File

@ -0,0 +1,21 @@
FROM golang:1-alpine as builder
RUN apk --update upgrade \
&& apk --no-cache --no-progress add git make gcc musl-dev
WORKDIR /go/src/github.com/ldez/traefik-certs-dumper
ENV GO111MODULE on
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN make build
FROM alpine:3.10
RUN apk --update upgrade \
&& apk --no-cache --no-progress add ca-certificates
COPY --from=builder /go/src/github.com/ldez/traefik-certs-dumper/traefik-certs-dumper /usr/bin/traefik-certs-dumper
ENTRYPOINT ["/usr/bin/traefik-certs-dumper"]

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");
you may not use this file except in compliance with the License.

View File

@ -25,3 +25,6 @@ checks:
doc:
go run . doc
publish-images:
seihon publish -v "$(TAG_NAME)" -v "latest" --image-name ldez/traefik-certs-dumper --dry-run=false

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,16 +1,14 @@
package cmd
import (
"context"
"time"
"github.com/kvtools/boltdb"
"github.com/abronan/valkeyrie/store"
"github.com/abronan/valkeyrie/store/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.
// boltdbCmd represents the boltdb command
var boltdbCmd = &cobra.Command{
Use: "boltdb",
Short: "Dump the content of BoltDB.",
@ -31,20 +29,11 @@ func boltdbRun(baseConfig *dumper.BaseConfig, cmd *cobra.Command) error {
return err
}
connectionTimeout, err := cmd.Flags().GetInt("connection-timeout")
if err != nil {
return err
}
config.Options.Bucket = cmd.Flag("bucket").Value.String()
config.Options.PersistConnection, _ = cmd.Flags().GetBool("persist-connection")
persistConnection, _ := cmd.Flags().GetBool("persist-connection")
config.Backend = store.BOLTDB
boltdb.Register()
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)
return kv.Dump(config, baseConfig)
}

View File

@ -1,16 +1,14 @@
package cmd
import (
"context"
"time"
"github.com/kvtools/consul"
"github.com/abronan/valkeyrie/store"
"github.com/abronan/valkeyrie/store/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.
// consulCmd represents the consul command
var consulCmd = &cobra.Command{
Use: "consul",
Short: "Dump the content of Consul.",
@ -30,24 +28,10 @@ func consulRun(baseConfig *dumper.BaseConfig, cmd *cobra.Command) error {
return err
}
tlsConfig, err := createTLSConfig(cmd)
if err != nil {
return err
}
config.Options.Token = cmd.Flag("token").Value.String()
connectionTimeout, err := cmd.Flags().GetInt("connection-timeout")
if err != nil {
return err
}
config.Backend = store.CONSUL
consul.Register()
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)
return kv.Dump(config, baseConfig)
}

View File

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

View File

@ -1,17 +1,16 @@
package cmd
import (
"context"
"time"
"github.com/kvtools/etcdv2"
"github.com/kvtools/etcdv3"
"github.com/abronan/valkeyrie/store"
"github.com/abronan/valkeyrie/store/etcd/v2"
"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.
// etcdCmd represents the etcd command
var etcdCmd = &cobra.Command{
Use: "etcd",
Short: "Dump the content of etcd.",
@ -23,7 +22,6 @@ 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 {
@ -32,47 +30,14 @@ func etcdRun(baseConfig *dumper.BaseConfig, cmd *cobra.Command) error {
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
}
config.Options.SyncPeriod = time.Duration(synPeriod) * time.Second
connectionTimeout, err := cmd.Flags().GetInt("connection-timeout")
if err != nil {
return err
}
config.Backend = store.ETCD
etcd.Register()
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)
return kv.Dump(config, baseConfig)
}

View File

@ -1,13 +1,12 @@
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"
)
// fileCmd represents the file command
var fileCmd = &cobra.Command{
Use: "file",
Short: `Dump the content of the "acme.json" file.`,
@ -15,9 +14,7 @@ var fileCmd = &cobra.Command{
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)
return file.Dump(acmeFile, baseConfig)
}),
}
@ -25,5 +22,5 @@ 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'.")
fileCmd.Flags().String("version", "", "Traefik version. If empty use v1. Possible values: 'v2'.")
}

View File

@ -3,16 +3,17 @@ package cmd
import (
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"time"
"github.com/abronan/valkeyrie/store"
"github.com/ldez/traefik-certs-dumper/v2/dumper/kv"
"github.com/spf13/cobra"
)
// kvCmd represents the kv command.
// kvCmd represents the kv command
var kvCmd = &cobra.Command{
Use: "kv",
Short: `Dump the content of a KV store.`,
@ -25,7 +26,6 @@ func init() {
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.")
@ -43,10 +43,25 @@ func getKvConfig(cmd *cobra.Command) (*kv.Config, error) {
return nil, err
}
connectionTimeout, err := cmd.Flags().GetInt("connection-timeout")
if err != nil {
return nil, err
}
tlsConfig, err := createTLSConfig(cmd)
if err != nil {
return nil, err
}
return &kv.Config{
Endpoints: endpoints,
Prefix: cmd.Flag("prefix").Value.String(),
Suffix: cmd.Flag("suffix").Value.String(),
Options: &store.Config{
ConnectionTimeout: time.Duration(connectionTimeout) * time.Second,
Username: cmd.Flag("password").Value.String(),
Password: cmd.Flag("username").Value.String(),
TLS: tlsConfig,
},
}, nil
}
@ -69,19 +84,19 @@ func createTLSConfig(cmd *cobra.Command) (*tls.Config, error) {
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")
if !insecureSkipVerify && (len(certContent) == 0 || len(privateKey) == 0) {
return nil, fmt.Errorf("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 nil, fmt.Errorf("failed to load TLS keypair: %s", err)
}
return &tls.Config{
Certificates: []tls.Certificate{cert},
RootCAs: caPool,
InsecureSkipVerify: insecureSkipVerify, //nolint:gosec // it's a CLI option.
InsecureSkipVerify: insecureSkipVerify,
ClientAuth: clientAuth,
}, nil
}
@ -92,11 +107,11 @@ func getCertPool(ca string) (*x509.CertPool, error) {
if ca != "" {
caContent, err := getCAContent(ca)
if err != nil {
return nil, fmt.Errorf("failed to read CA. %w", err)
return nil, fmt.Errorf("failed to read CA. %s", err)
}
if !caPool.AppendCertsFromPEM(caContent) {
return nil, errors.New("failed to parse CA")
return nil, fmt.Errorf("failed to parse CA")
}
}
@ -104,14 +119,11 @@ func getCertPool(ca string) (*x509.CertPool, error) {
}
func getCAContent(ca string) ([]byte, error) {
if _, err := os.Stat(ca); err != nil {
if os.IsNotExist(err) {
return []byte(ca), nil
}
return nil, err
if _, errCA := os.Stat(ca); errCA != nil {
return []byte(ca), nil
}
caContent, err := os.ReadFile(filepath.Clean(ca))
caContent, err := ioutil.ReadFile(ca)
if err != nil {
return nil, err
}
@ -138,11 +150,11 @@ func getCertificate(privateKey, certContent string) (tls.Certificate, error) {
_, 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")
return tls.Certificate{}, fmt.Errorf("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")
return tls.Certificate{}, fmt.Errorf("TLS key is a file, but tls cert is not")
}
// string

View File

@ -1,30 +1,27 @@
package cmd
import (
"crypto/rand"
"fmt"
"io/ioutil"
"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"
homedir "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.
// 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 {
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
if cmd.Name() == "version" {
return nil
}
@ -38,7 +35,6 @@ var rootCmd = &cobra.Command{
return fmt.Errorf("--crt-ext (%q) and --key-ext (%q) are identical, in this case --domain-subdir is required", crtExt, keyExt)
}
}
return nil
},
}
@ -50,8 +46,6 @@ func Execute() {
log.Println(err)
os.Exit(1)
}
help()
}
func init() {
@ -115,7 +109,7 @@ func runE(apply func(*dumper.BaseConfig, *cobra.Command) error) func(*cobra.Comm
func tree(root, indent string) error {
fi, err := os.Stat(root)
if err != nil {
return fmt.Errorf("could not stat %s: %w", root, err)
return fmt.Errorf("could not stat %s: %v", root, err)
}
fmt.Println(fi.Name())
@ -123,9 +117,9 @@ func tree(root, indent string) error {
return nil
}
fis, err := os.ReadDir(root)
fis, err := ioutil.ReadDir(root)
if err != nil {
return fmt.Errorf("could not read dir %s: %w", root, err)
return fmt.Errorf("could not read dir %s: %v", root, err)
}
var names []string
@ -138,10 +132,10 @@ func tree(root, indent string) error {
for i, name := range names {
add := "│ "
if i == len(names)-1 {
fmt.Print(indent + "└──")
fmt.Printf(indent + "└──")
add = " "
} else {
fmt.Print(indent + "├──")
fmt.Printf(indent + "├──")
}
if err := tree(filepath.Join(root, name), indent+add); err != nil {
@ -184,34 +178,3 @@ func getBaseConfig(cmd *cobra.Command) (*dumper.BaseConfig, error) {
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

@ -13,11 +13,11 @@ var (
date = "I don't remember exactly"
)
// versionCmd represents the version command.
// versionCmd represents the version command
var versionCmd = &cobra.Command{
Use: "version",
Short: "Display version",
Run: func(_ *cobra.Command, _ []string) {
Run: func(cmd *cobra.Command, args []string) {
displayVersion(rootCmd.Name())
},
}

View File

@ -1,16 +1,14 @@
package cmd
import (
"context"
"time"
"github.com/kvtools/zookeeper"
"github.com/abronan/valkeyrie/store"
"github.com/abronan/valkeyrie/store/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.
// zookeeperCmd represents the zookeeper command
var zookeeperCmd = &cobra.Command{
Use: "zookeeper",
Short: "Dump the content of zookeeper.",
@ -28,19 +26,8 @@ func zookeeperRun(baseConfig *dumper.BaseConfig, cmd *cobra.Command) error {
return err
}
connectionTimeout, err := cmd.Flags().GetInt("connection-timeout")
if err != nil {
return err
}
config.Backend = store.ZK
zookeeper.Register()
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)
return kv.Dump(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

@ -24,9 +24,8 @@ Dump Let's Encrypt certificates from Traefik.
### 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
###### Auto generated by spf13/cobra on 1-Sep-2019

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

@ -15,7 +15,7 @@ traefik-certs-dumper file [flags]
```
-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'.
--version string Traefik version. If empty use v1. Possible values: 'v2'.
```
### Options inherited from parent commands
@ -37,4 +37,4 @@ traefik-certs-dumper file [flags]
* [traefik-certs-dumper](traefik-certs-dumper.md) - Dump Let's Encrypt certificates from Traefik.
###### Auto generated by spf13/cobra on 21-Feb-2025
###### Auto generated by spf13/cobra on 1-Sep-2019

View File

@ -14,7 +14,6 @@ Dump the content of a KV store.
-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
@ -47,4 +46,4 @@ Dump the content of a KV store.
* [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
###### Auto generated by spf13/cobra on 1-Sep-2019

View File

@ -34,7 +34,6 @@ traefik-certs-dumper kv boltdb [flags]
--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
@ -49,4 +48,4 @@ traefik-certs-dumper kv boltdb [flags]
* [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
###### Auto generated by spf13/cobra on 1-Sep-2019

View File

@ -33,7 +33,6 @@ traefik-certs-dumper kv consul [flags]
--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
@ -48,4 +47,4 @@ traefik-certs-dumper kv consul [flags]
* [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
###### Auto generated by spf13/cobra on 1-Sep-2019

View File

@ -13,9 +13,8 @@ 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.
-h, --help help for etcd
--sync-period int Sync period for etcd in seconds.
```
### Options inherited from parent commands
@ -34,7 +33,6 @@ traefik-certs-dumper kv etcd [flags]
--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
@ -49,4 +47,4 @@ traefik-certs-dumper kv etcd [flags]
* [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
###### Auto generated by spf13/cobra on 1-Sep-2019

View File

@ -32,7 +32,6 @@ traefik-certs-dumper kv zookeeper [flags]
--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
@ -47,4 +46,4 @@ traefik-certs-dumper kv zookeeper [flags]
* [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
###### Auto generated by spf13/cobra on 1-Sep-2019

View File

@ -2,6 +2,10 @@
Display version
### Synopsis
Display version
```
traefik-certs-dumper version [flags]
```
@ -31,4 +35,4 @@ traefik-certs-dumper version [flags]
* [traefik-certs-dumper](traefik-certs-dumper.md) - Dump Let's Encrypt certificates from Traefik.
###### Auto generated by spf13/cobra on 21-Feb-2025
###### Auto generated by spf13/cobra on 1-Sep-2019

View File

@ -2,133 +2,76 @@ package file
import (
"bytes"
"context"
"crypto/sha256"
"crypto/md5"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"os"
"path/filepath"
"strings"
"github.com/containous/traefik/v2/pkg/provider/acme"
"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"
v1 "github.com/ldez/traefik-certs-dumper/v2/dumper/v1"
v2 "github.com/ldez/traefik-certs-dumper/v2/dumper/v2"
"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 {
func Dump(acmeFile string, baseConfig *dumper.BaseConfig) error {
err := dump(acmeFile, baseConfig)
if err != nil {
return err
}
if baseConfig.Watch {
hook.Exec(ctx, baseConfig.Hook)
hook.Exec(baseConfig.Hook)
return watch(ctx, acmeFile, baseConfig)
return watch(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
if baseConfig.Version == "v2" {
return dumpV2(acmeFile, baseConfig)
}
return dumpV1(acmeFile, baseConfig)
}
func dumpV1(acmeFile string, baseConfig *dumper.BaseConfig) error {
data := &traefikv1.StoredData{}
err := readJSONFile(acmeFile, data)
source, err := os.Open(acmeFile)
if err != nil {
return err
}
return dumperv1.Dump(data, baseConfig)
data := &v1.StoredData{}
if err = json.NewDecoder(source).Decode(data); err != nil {
return err
}
return v1.Dump(data, baseConfig)
}
func dumpV2(acmeFile string, baseConfig *dumper.BaseConfig) error {
data := map[string]*traefikv2.StoredData{}
err := readJSONFile(acmeFile, &data)
source, err := os.Open(acmeFile)
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 {
data := &acme.StoredData{}
if err = json.NewDecoder(source).Decode(data); err != nil {
return err
}
return dumperv3.Dump(data, baseConfig)
return v2.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 {
func watch(acmeFile string, baseConfig *dumper.BaseConfig) error {
watcher, err := fsnotify.NewWatcher()
if err != nil {
return fmt.Errorf("failed to create new watcher: %w", err)
return err
}
defer func() { _ = watcher.Close() }()
@ -148,7 +91,7 @@ func watch(ctx context.Context, acmeFile string, baseConfig *dumper.BaseConfig)
log.Println("event:", event)
}
hash, errW := manageEvent(ctx, watcher, event, acmeFile, previousHash, baseConfig)
hash, errW := manageEvent(watcher, event, acmeFile, previousHash, baseConfig)
if errW != nil {
log.Println("error:", errW)
done <- true
@ -171,7 +114,7 @@ func watch(ctx context.Context, acmeFile string, baseConfig *dumper.BaseConfig)
err = watcher.Add(acmeFile)
if err != nil {
return fmt.Errorf("failed to add a new watcher: %w", err)
return err
}
<-done
@ -179,15 +122,15 @@ func watch(ctx context.Context, acmeFile string, baseConfig *dumper.BaseConfig)
return nil
}
func manageEvent(ctx context.Context, watcher *fsnotify.Watcher, event fsnotify.Event, acmeFile string, previousHash []byte, baseConfig *dumper.BaseConfig) ([]byte, error) {
func manageEvent(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)
return nil, err
}
hash, err := calculateHash(acmeFile)
if err != nil {
return nil, fmt.Errorf("file hash calculation failed: %w", err)
return nil, err
}
if !bytes.Equal(previousHash, hash) {
@ -203,7 +146,7 @@ func manageEvent(ctx context.Context, watcher *fsnotify.Watcher, event fsnotify.
log.Println("Dumped new certificate data.")
}
hook.Exec(ctx, baseConfig.Hook)
hook.Exec(baseConfig.Hook)
}
return hash, nil
@ -222,13 +165,13 @@ func manageRename(watcher *fsnotify.Watcher, event fsnotify.Event, acmeFile stri
}
func calculateHash(acmeFile string) ([]byte, error) {
file, err := os.Open(filepath.Clean(acmeFile))
file, err := os.Open(acmeFile)
if err != nil {
return nil, err
}
defer func() { _ = file.Close() }()
h := sha256.New()
h := md5.New()
_, err = io.Copy(h, file)
if err != nil {
return nil, err

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 +1,11 @@
package kv
import (
"github.com/kvtools/valkeyrie"
)
import "github.com/abronan/valkeyrie/store"
// Config KV configuration.
type Config struct {
StoreName string
Backend store.Backend
Prefix string
Suffix string
Endpoints []string
Options valkeyrie.Config
Options *store.Config
}

View File

@ -1,12 +1,12 @@
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"
"github.com/go-acme/lego/v3/certcrypto"
"github.com/go-acme/lego/v3/registration"
v1 "github.com/ldez/traefik-certs-dumper/v2/dumper/v1"
)
// CertificateOld is used to store certificate info.
// CertificateOld is used to store certificate info
type CertificateOld struct {
Domain string
CertURL string
@ -15,7 +15,7 @@ type CertificateOld struct {
Certificate []byte
}
// AccountOld is used to store lets encrypt registration info.
// AccountOld is used to store lets encrypt registration info
type AccountOld struct {
Email string
Registration *registration.Resource
@ -26,43 +26,40 @@ type AccountOld struct {
HTTPChallenge map[string]map[string][]byte
}
// DomainsCertificates stores a certificate for multiple domains.
// DomainsCertificates stores a certificate for multiple domains
type DomainsCertificates struct {
Certs []*DomainsCertificate
}
// ChallengeCert stores a challenge certificate.
// ChallengeCert stores a challenge certificate
type ChallengeCert struct {
Certificate []byte
PrivateKey []byte
}
// DomainsCertificate contains a certificate for multiple domains.
// DomainsCertificate contains a certificate for multiple domains
type DomainsCertificate struct {
Domains traefikv1.Domain
Domains v1.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,
},
func convertOldAccount(account *AccountOld) *v1.StoredData {
storedData := &v1.StoredData{}
storedData.Account = &v1.Account{
PrivateKey: account.PrivateKey,
Registration: account.Registration,
Email: account.Email,
KeyType: account.KeyType,
}
var certs []*traefikv1.Certificate
var certs []*v1.Certificate
for _, oldCert := range account.DomainsCertificate.Certs {
certs = append(certs, &traefikv1.Certificate{
certs = append(certs, &v1.Certificate{
Certificate: oldCert.Certificate.Certificate,
Domain: oldCert.Domains,
Key: oldCert.Certificate.PrivateKey,
})
}
storedData.Certificates = certs
return storedData
}

View File

@ -3,48 +3,47 @@ package kv
import (
"bytes"
"compress/gzip"
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"strings"
"github.com/kvtools/valkeyrie"
"github.com/kvtools/valkeyrie/store"
"github.com/abronan/valkeyrie"
"github.com/abronan/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"
const storeKeySuffix = "/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)
func Dump(config *Config, baseConfig *dumper.BaseConfig) error {
kvStore, err := valkeyrie.NewStore(config.Backend, config.Endpoints, config.Options)
if err != nil {
return fmt.Errorf("unable to create client of the store: %w", err)
return fmt.Errorf("unable to create client of the store: %v", err)
}
storeKey := config.Prefix + config.Suffix
storeKey := config.Prefix + storeKeySuffix
if baseConfig.Watch {
return watch(ctx, kvStore, storeKey, baseConfig)
return watch(kvStore, storeKey, baseConfig)
}
pair, err := kvStore.Get(ctx, storeKey, nil)
pair, err := kvStore.Get(storeKey, nil)
if err != nil {
return fmt.Errorf("unable to retrieve %s value: %w", storeKey, err)
return fmt.Errorf("unable to retrieve %s value: %v", 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)
func watch(kvStore store.Store, storeKey string, baseConfig *dumper.BaseConfig) error {
stopCh := make(<-chan struct{})
pairs, err := kvStore.Watch(storeKey, stopCh, nil)
if err != nil {
return err
}
@ -64,7 +63,7 @@ func watch(ctx context.Context, kvStore store.Store, storeKey string, baseConfig
log.Println("Dumped new certificate data.")
}
hook.Exec(ctx, baseConfig.Hook)
hook.Exec(baseConfig.Hook)
}
}
@ -77,21 +76,20 @@ func dumpPair(pair *store.KVPair, baseConfig *dumper.BaseConfig) error {
return v1.Dump(data, baseConfig)
}
func getStoredDataFromGzip(pair *store.KVPair) (*traefikv1.StoredData, error) {
func getStoredDataFromGzip(pair *store.KVPair) (*v1.StoredData, error) {
reader, err := gzip.NewReader(bytes.NewBuffer(pair.Value))
if err != nil {
return nil, fmt.Errorf("fail to create GZip reader: %w", err)
return nil, fmt.Errorf("fail to create GZip reader: %v", err)
}
acmeData, err := io.ReadAll(reader)
acmeData, err := ioutil.ReadAll(reader)
if err != nil {
return nil, fmt.Errorf("unable to read the pair content: %w", err)
return nil, fmt.Errorf("unable to read the pair content: %v", 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 nil, fmt.Errorf("unable marshal AccountOld: %v", err)
}
return convertOldAccount(account), nil

View File

@ -2,13 +2,12 @@ package v1
import (
"encoding/pem"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"github.com/go-acme/lego/v4/certcrypto"
"github.com/go-acme/lego/v3/certcrypto"
"github.com/ldez/traefik-certs-dumper/v2/dumper"
"github.com/ldez/traefik-certs-dumper/v2/internal/traefikv1"
)
const (
@ -17,33 +16,33 @@ const (
)
// Dump Dumps data to certificates.
func Dump(data *traefikv1.StoredData, baseConfig *dumper.BaseConfig) error {
func Dump(data *StoredData, baseConfig *dumper.BaseConfig) error {
if baseConfig.Clean {
err := cleanDir(baseConfig.DumpPath)
if err != nil {
return fmt.Errorf("folder cleaning failed: %w", err)
return 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, certsSubDir), 0755); err != nil {
return err
}
}
if err := os.MkdirAll(filepath.Join(baseConfig.DumpPath, keysSubDir), 0o755); err != nil {
return fmt.Errorf("keys folder creation failure: %w", err)
if err := os.MkdirAll(filepath.Join(baseConfig.DumpPath, keysSubDir), 0755); err != nil {
return 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)
return err
}
err = writeKey(baseConfig.DumpPath, cert, baseConfig.KeyInfo, baseConfig.DomainSubDir)
if err != nil {
return fmt.Errorf("failed to write certificate keys: %w", err)
return err
}
}
@ -52,34 +51,34 @@ func Dump(data *traefikv1.StoredData, baseConfig *dumper.BaseConfig) error {
}
privateKeyPem := extractPEMPrivateKey(data.Account)
return os.WriteFile(filepath.Join(baseConfig.DumpPath, keysSubDir, "letsencrypt"+baseConfig.KeyInfo.Ext), privateKeyPem, 0o600)
return ioutil.WriteFile(filepath.Join(baseConfig.DumpPath, keysSubDir, "letsencrypt"+baseConfig.KeyInfo.Ext), privateKeyPem, 0600)
}
func writeCert(dumpPath string, cert *traefikv1.Certificate, info dumper.FileInfo, domainSubDir bool) error {
func writeCert(dumpPath string, cert *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 {
if err := os.MkdirAll(filepath.Join(dumpPath, safeName(cert.Domain.Main)), 0755); err != nil {
return err
}
}
return os.WriteFile(certPath, cert.Certificate, 0o666)
return ioutil.WriteFile(certPath, cert.Certificate, 0666)
}
func writeKey(dumpPath string, cert *traefikv1.Certificate, info dumper.FileInfo, domainSubDir bool) error {
func writeKey(dumpPath string, cert *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 {
if err := os.MkdirAll(filepath.Join(dumpPath, safeName(cert.Domain.Main)), 0755); err != nil {
return err
}
}
return os.WriteFile(keyPath, cert.Key, 0o600)
return ioutil.WriteFile(keyPath, cert.Key, 0600)
}
func extractPEMPrivateKey(account *traefikv1.Account) []byte {
func extractPEMPrivateKey(account *Account) []byte {
var block *pem.Block
switch account.KeyType {
case certcrypto.RSA2048, certcrypto.RSA4096, certcrypto.RSA8192:
@ -93,7 +92,7 @@ func extractPEMPrivateKey(account *traefikv1.Account) []byte {
Bytes: account.PrivateKey,
}
default:
panic(fmt.Sprintf("unsupported key type: '%v'", account.KeyType))
panic("unsupported key type")
}
return pem.EncodeToMemory(block)
@ -109,7 +108,7 @@ func cleanDir(dumpPath string) error {
return errExists
}
dir, err := os.ReadDir(dumpPath)
dir, err := ioutil.ReadDir(dumpPath)
if err != nil {
return err
}

View File

@ -1,4 +1,3 @@
//go:build !windows
// +build !windows
package v1

View File

@ -1,11 +1,11 @@
package traefikv1
package v1
import (
"github.com/go-acme/lego/v4/certcrypto"
"github.com/go-acme/lego/v4/registration"
"github.com/go-acme/lego/v3/certcrypto"
"github.com/go-acme/lego/v3/registration"
)
// StoredData represents the data managed by the Store.
// StoredData represents the data managed by the Store
type StoredData struct {
Account *Account
Certificates []*Certificate
@ -13,20 +13,20 @@ type StoredData struct {
TLSChallenges map[string]*Certificate
}
// Certificate is a struct which contains all data needed from an ACME 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.
// Domain holds a domain name with SANs
type Domain struct {
Main string
SANs []string
}
// Account is used to store lets encrypt registration info.
// Account is used to store lets encrypt registration info
type Account struct {
Email string
Registration *registration.Resource

View File

@ -2,13 +2,13 @@ package v2
import (
"encoding/pem"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"github.com/go-acme/lego/v4/certcrypto"
"github.com/containous/traefik/v2/pkg/provider/acme"
"github.com/go-acme/lego/v3/certcrypto"
"github.com/ldez/traefik-certs-dumper/v2/dumper"
"github.com/ldez/traefik-certs-dumper/v2/internal/traefikv2"
)
const (
@ -17,77 +17,69 @@ const (
)
// Dump Dumps data to certificates.
func Dump(data map[string]*traefikv2.StoredData, baseConfig *dumper.BaseConfig) error {
func Dump(data *acme.StoredData, baseConfig *dumper.BaseConfig) error {
if baseConfig.Clean {
err := cleanDir(baseConfig.DumpPath)
if err != nil {
return fmt.Errorf("folder cleaning failed: %w", err)
return 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, certsSubDir), 0755); err != nil {
return err
}
}
if err := os.MkdirAll(filepath.Join(baseConfig.DumpPath, keysSubDir), 0o755); err != nil {
return fmt.Errorf("keys folder creation failure: %w", err)
if err := os.MkdirAll(filepath.Join(baseConfig.DumpPath, keysSubDir), 0755); err != nil {
return 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)
for _, cert := range data.Certificates {
err := writeCert(baseConfig.DumpPath, cert.Certificate, baseConfig.CrtInfo, baseConfig.DomainSubDir)
if err != nil {
return fmt.Errorf("failed to write private key: %w", err)
return err
}
err = writeKey(baseConfig.DumpPath, cert.Certificate, baseConfig.KeyInfo, baseConfig.DomainSubDir)
if err != nil {
return err
}
}
return nil
if data.Account == nil {
return nil
}
privateKeyPem := extractPEMPrivateKey(data.Account)
return ioutil.WriteFile(filepath.Join(baseConfig.DumpPath, keysSubDir, "letsencrypt"+baseConfig.KeyInfo.Ext), privateKeyPem, 0600)
}
func writeCert(dumpPath string, cert traefikv2.Certificate, info dumper.FileInfo, domainSubDir bool) error {
func writeCert(dumpPath string, cert acme.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 {
if err := os.MkdirAll(filepath.Join(dumpPath, safeName(cert.Domain.Main)), 0755); err != nil {
return err
}
}
return os.WriteFile(certPath, cert.Certificate, 0o666)
return ioutil.WriteFile(certPath, cert.Certificate, 0666)
}
func writeKey(dumpPath string, cert traefikv2.Certificate, info dumper.FileInfo, domainSubDir bool) error {
func writeKey(dumpPath string, cert acme.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 {
if err := os.MkdirAll(filepath.Join(dumpPath, safeName(cert.Domain.Main)), 0755); err != nil {
return err
}
}
return os.WriteFile(keyPath, cert.Key, 0o600)
return ioutil.WriteFile(keyPath, cert.Key, 0600)
}
func extractPEMPrivateKey(account *traefikv2.Account) []byte {
func extractPEMPrivateKey(account *acme.Account) []byte {
var block *pem.Block
switch account.KeyType {
case certcrypto.RSA2048, certcrypto.RSA4096, certcrypto.RSA8192:
@ -101,7 +93,7 @@ func extractPEMPrivateKey(account *traefikv2.Account) []byte {
Bytes: account.PrivateKey,
}
default:
panic(fmt.Sprintf("unsupported key type: '%v'", account.KeyType))
panic("unsupported key type")
}
return pem.EncodeToMemory(block)
@ -117,7 +109,7 @@ func cleanDir(dumpPath string) error {
return errExists
}
dir, err := os.ReadDir(dumpPath)
dir, err := ioutil.ReadDir(dumpPath)
if err != nil {
return err
}

View File

@ -1,4 +1,3 @@
//go:build !windows
// +build !windows
package v2

35
dumper/v2/storeddata.go Normal file
View File

@ -0,0 +1,35 @@
package v2
import (
"github.com/go-acme/lego/v3/certcrypto"
"github.com/go-acme/lego/v3/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 `json:"domain,omitempty"`
Certificate []byte `json:"certificate,omitempty"`
Key []byte `json:"key,omitempty"`
}
// Domain holds a domain name with SANs
type Domain struct {
Main string `json:"main,omitempty"`
SANs []string `json:"sans,omitempty"`
}
// 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,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, "*", "_")
}

104
go.mod
View File

@ -1,90 +1,32 @@
module github.com/ldez/traefik-certs-dumper/v2
go 1.24.0
go 1.12
require (
github.com/charmbracelet/lipgloss v1.0.0
github.com/fsnotify/fsnotify v1.9.0
github.com/go-acme/lego/v4 v4.25.2
github.com/kvtools/boltdb v1.0.2
github.com/kvtools/consul v1.0.2
github.com/kvtools/etcdv2 v1.0.2
github.com/kvtools/etcdv3 v1.0.2
github.com/kvtools/valkeyrie v1.0.0
github.com/kvtools/zookeeper v1.0.2
github.com/abronan/valkeyrie v0.0.0-20190822142731-f2e1850dc905
github.com/containous/traefik/v2 v2.0.0
github.com/fsnotify/fsnotify v1.4.8-0.20190312181446-1485a34d5d57
github.com/go-acme/lego/v3 v3.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
github.com/spf13/cobra v0.0.5
github.com/spf13/viper v1.4.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
// related to valkeyrie
replace github.com/hashicorp/consul => github.com/hashicorp/consul v1.6.0
// related to Traefik
replace (
github.com/Azure/go-autorest => github.com/Azure/go-autorest v12.4.1+incompatible
github.com/docker/docker => github.com/docker/engine v0.0.0-20190725163905-fa8dd90ceb7b
)
exclude github.com/tencentcloud/tencentcloud-sdk-go v3.0.83+incompatible
// related to Traefik: Containous forks
replace (
github.com/abbot/go-http-auth => github.com/containous/go-http-auth v0.4.1-0.20180112153951-65b0cdae8d7f
github.com/go-check/check => github.com/containous/check v0.0.0-20170915194414-ca0bf163426a
github.com/gorilla/mux => github.com/containous/mux v0.0.0-20181024131434-c33f32e26898
github.com/mailgun/minheap => github.com/containous/minheap v0.0.0-20190809180810-6e71eb837595
github.com/mailgun/multibuf => github.com/containous/multibuf v0.0.0-20190809014333-8b6c9a7e6bba
github.com/rancher/go-rancher-metadata => github.com/containous/go-rancher-metadata v0.0.0-20190402144056-c6a65f8b7a28
)

1075
go.sum

File diff suppressed because it is too large Load Diff

View File

@ -11,21 +11,21 @@ import (
)
// Exec Execute a command on a go routine.
func Exec(ctx context.Context, command string) {
func Exec(command string) {
if command == "" {
return
}
go func() {
errH := execute(ctx, command)
errH := execute(command)
if errH != nil {
panic(errH)
}
}()
}
func execute(ctx context.Context, command string) error {
ctxCmd, cancel := context.WithTimeout(ctx, 30*time.Second)
func execute(command string) error {
ctxCmd, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
parts := strings.Fields(os.ExpandEnv(command))
@ -34,7 +34,7 @@ func execute(ctx context.Context, command string) error {
fmt.Println(string(output))
}
if errors.Is(ctxCmd.Err(), context.DeadlineExceeded) {
if ctxCmd.Err() == context.DeadlineExceeded {
return errors.New("hook timed out")
}

View File

@ -1,8 +1,6 @@
package hook
import (
"testing"
)
import "testing"
func Test_execute(t *testing.T) {
testCases := []struct {
@ -21,7 +19,8 @@ func Test_execute(t *testing.T) {
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
err := execute(t.Context(), test.command)
err := execute(test.command)
if err != nil {
t.Fatal(err)
}

View File

@ -3,17 +3,16 @@ package main
import (
"bytes"
"compress/gzip"
"context"
"io/ioutil"
"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"
"github.com/abronan/valkeyrie"
"github.com/abronan/valkeyrie/store"
"github.com/abronan/valkeyrie/store/boltdb"
"github.com/abronan/valkeyrie/store/consul"
etcdv3 "github.com/abronan/valkeyrie/store/etcd/v3"
"github.com/abronan/valkeyrie/store/zookeeper"
)
const storeKey = "traefik/acme/account/object"
@ -22,43 +21,38 @@ func main() {
log.SetFlags(log.Lshortfile)
source := "./acme.json"
err := loadData(context.Background(), source)
err := loadData(source)
if err != nil {
log.Fatal(err)
}
}
func loadData(ctx context.Context, source string) error {
func loadData(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)
err = putData(store.CONSUL, []string{"localhost:8500"}, content)
if err != nil {
return err
}
// ETCD v3
err = putData(ctx, etcdv3.StoreName, []string{"localhost:2379"},
&etcdv3.Config{ConnectionTimeout: 3 * time.Second}, content)
err = putData(store.ETCDV3, []string{"localhost:2379"}, content)
if err != nil {
return err
}
// Zookeeper
err = putData(ctx, zookeeper.StoreName, []string{"localhost:2181"},
&zookeeper.Config{ConnectionTimeout: 3 * time.Second}, content)
err = putData(store.ZK, []string{"localhost:2181"}, 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)
err = putData(store.BOLTDB, []string{"/tmp/test-traefik-certs-dumper.db"}, content)
if err != nil {
return err
}
@ -66,13 +60,29 @@ func loadData(ctx context.Context, source string) error {
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)
func putData(backend store.Backend, addrs []string, content []byte) error {
storeConfig := &store.Config{
ConnectionTimeout: 3 * time.Second,
Bucket: "traefik",
}
switch backend {
case store.CONSUL:
consul.Register()
case store.ETCDV3:
etcdv3.Register()
case store.ZK:
zookeeper.Register()
case store.BOLTDB:
boltdb.Register()
}
kvStore, err := valkeyrie.NewStore(backend, addrs, storeConfig)
if err != nil {
return err
}
if err := kvStore.Put(ctx, storeKey, content, nil); err != nil {
if err := kvStore.Put(storeKey, content, nil); err != nil {
return err
}
@ -81,7 +91,7 @@ func putData(ctx context.Context, backend string, addrs []string, options valkey
}
func readFile(source string) ([]byte, error) {
content, err := os.ReadFile(filepath.Clean(source))
content, err := ioutil.ReadFile(source)
if err != nil {
return nil, err
}

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
}

View File

@ -1,13 +1,13 @@
# 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)
[![Build Status](https://github.com/ldez/traefik-certs-dumper/workflows/Main/badge.svg?branch=master)](https://github.com/ldez/traefik-certs-dumper/actions)
[![Docker Image Version (latest semver)](https://img.shields.io/docker/v/ldez/traefik-certs-dumper)](https://hub.docker.com/r/ldez/traefik-certs-dumper/)
[![Build Status](https://travis-ci.org/ldez/traefik-certs-dumper.svg?branch=master)](https://travis-ci.org/ldez/traefik-certs-dumper)
[![Docker Information](https://images.microbadger.com/badges/image/ldez/traefik-certs-dumper.svg)](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)
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)
[![Say Thanks!](https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg?style=for-the-badge)](https://saythanks.io/to/ldez)
## Features
@ -18,17 +18,16 @@ If you appreciate this project:
- from file ("acme.json")
- from KV stores (Consul, Etcd, Zookeeper)
- Output formats:
- use domain as subdirectory (allow custom names and extensions)
- use domain as sub-directory (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
### Download / CI Integration
```bash
curl -sfL https://raw.githubusercontent.com/ldez/traefik-certs-dumper/master/godownloader.sh | bash -s -- -b $(go env GOPATH)/bin v2.9.3
curl -sfL https://raw.githubusercontent.com/ldez/traefik-certs-dumper/master/godownloader.sh | bash -s -- -b $GOPATH/bin v1.5.0
```
<!--
@ -57,12 +56,6 @@ You can use pre-compiled binaries:
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)
@ -74,7 +67,7 @@ Examples:
### Simple Dump
```console
$ traefik-certs-dumper file --version v3
$ traefik-certs-dumper file
dump
├──certs
│ └──my.domain.com.key
@ -86,7 +79,7 @@ dump
### Change source and destination
```console
$ traefik-certs-dumper file --version v3 --source ./acme.json --dest ./dump/test
$ traefik-certs-dumper file --source ./acme.json --dest ./dump/test
test
├──certs
│ └──my.domain.com.key
@ -98,7 +91,7 @@ test
### Use domain as sub-directory
```console
$ traefik-certs-dumper file --version v3 --domain-subdir=true
$ traefik-certs-dumper file --domain-subdir=true
dump
├──my.domain.com
│ ├──certificate.crt
@ -110,7 +103,7 @@ dump
#### Change file extension
```console
$ traefik-certs-dumper file --version v3 --domain-subdir --crt-ext=.pem --key-ext=.pem
$ traefik-certs-dumper file --domain-subdir --crt-ext=.pem --key-ext=.pem
dump
├──my.domain.com
│ ├──certificate.pem
@ -122,7 +115,7 @@ dump
#### Change file name
```console
$ traefik-certs-dumper file --version v3 --domain-subdir --crt-name=fullchain --key-name=privkey
$ traefik-certs-dumper file --domain-subdir --crt-name=fullchain --key-name=privkey
dump
├──my.domain.com
│ ├──fullchain.crt
@ -131,37 +124,6 @@ dump
└──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
@ -187,3 +149,5 @@ $ traefik-certs-dumper kv boltdb --endpoints /the/path/to/mydb.db
```console
$ traefik-certs-dumper kv zookeeper --endpoints localhost:2181
```

24
tmpl.Dockerfile Normal file
View File

@ -0,0 +1,24 @@
FROM golang:1-alpine as builder
RUN apk --update upgrade \
&& apk --no-cache --no-progress add git make gcc musl-dev ca-certificates tzdata
WORKDIR /go/src/github.com/ldez/traefik-certs-dumper
ENV GO111MODULE on
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN GOARCH={{ .GoARCH }} GOARM={{ .GoARM }} make build
FROM {{ .RuntimeImage }}
# Not supported for multi-arch without Buildkit or QEMU
#RUN apk --update upgrade \
# && apk --no-cache --no-progress add ca-certificates
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /go/src/github.com/ldez/traefik-certs-dumper/traefik-certs-dumper /usr/bin/traefik-certs-dumper
ENTRYPOINT ["/usr/bin/traefik-certs-dumper"]