Skip to content

Commit

Permalink
feat: dedicated Traefik v3 implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
ldez committed Nov 8, 2024
1 parent cffec51 commit 520caaf
Show file tree
Hide file tree
Showing 10 changed files with 576 additions and 323 deletions.
2 changes: 1 addition & 1 deletion cmd/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,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'.")
fileCmd.Flags().String("version", "", "Traefik version. If empty use v1. Possible values: 'v2', 'v3'.")
}
59 changes: 46 additions & 13 deletions dumper/file/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ import (

"github.com/fsnotify/fsnotify"
"github.com/ldez/traefik-certs-dumper/v2/dumper"
v1 "github.com/ldez/traefik-certs-dumper/v2/dumper/v1"
v2 "github.com/ldez/traefik-certs-dumper/v2/dumper/v2"
dumperv1 "github.com/ldez/traefik-certs-dumper/v2/dumper/v1"
dumperv2 "github.com/ldez/traefik-certs-dumper/v2/dumper/v2"
dumperv3 "github.com/ldez/traefik-certs-dumper/v2/dumper/v3"
"github.com/ldez/traefik-certs-dumper/v2/hook"
"github.com/traefik/traefik/v2/pkg/provider/acme"
acmev2 "github.com/traefik/traefik/v2/pkg/provider/acme"
acmev3 "github.com/traefik/traefik/v3/pkg/provider/acme"
)

// Dump Dumps "acme.json" file to certificates.
Expand All @@ -33,43 +35,74 @@ func Dump(ctx context.Context, acmeFile string, baseConfig *dumper.BaseConfig) e

return watch(ctx, acmeFile, baseConfig)
}

return nil
}

func dump(acmeFile string, baseConfig *dumper.BaseConfig) error {
if baseConfig.Version == "v2" {
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
}

err := dumpV1(acmeFile, baseConfig)
if err != nil {
return fmt.Errorf("v1: dump failed: %w", err)
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
}
return nil
}

func dumpV1(acmeFile string, baseConfig *dumper.BaseConfig) error {
data := &v1.StoredData{}
data := &dumperv1.StoredData{}
err := readJSONFile(acmeFile, data)
if err != nil {
return err
}

return v1.Dump(data, baseConfig)
return dumperv1.Dump(data, baseConfig)
}

func dumpV2(acmeFile string, baseConfig *dumper.BaseConfig) error {
data := map[string]*acme.StoredData{}
data := map[string]*acmev2.StoredData{}
err := readJSONFile(acmeFile, &data)
if err != nil {
return err
}

return dumperv2.Dump(data, baseConfig)
}

func dumpV3(acmeFile string, baseConfig *dumper.BaseConfig) error {
data := map[string]*acmev3.StoredData{}
err := readJSONFile(acmeFile, &data)
if err != nil {
return err
}

return v2.Dump(data, baseConfig)
return dumperv3.Dump(data, baseConfig)
}

func readJSONFile(acmeFile string, data interface{}) error {
Expand Down
5 changes: 5 additions & 0 deletions dumper/file/file_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ func TestDump(t *testing.T) {
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 {
Expand Down
36 changes: 36 additions & 0 deletions dumper/file/fixtures/acme-v3.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"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"
}
]
}
}
132 changes: 132 additions & 0 deletions dumper/v3/dumper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
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/traefik/traefik/v3/pkg/provider/acme"
)

const (
certsSubDir = "certs"
keysSubDir = "private"
)

// Dump Dumps data to certificates.
func Dump(data map[string]*acme.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 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 {
return err
}
}

return os.WriteFile(certPath, cert.Certificate, 0o666)
}

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 {
return err
}
}

return os.WriteFile(keyPath, cert.Key, 0o600)
}

func extractPEMPrivateKey(account *acme.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
}
8 changes: 8 additions & 0 deletions dumper/v3/filename.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
//go:build !windows
// +build !windows

package v3

func safeName(filename string) string {
return filename
}
10 changes: 10 additions & 0 deletions dumper/v3/filename_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//go:build windows
// +build windows

package v3

import "strings"

func safeName(filename string) string {
return strings.ReplaceAll(filename, "*", "_")
}
Loading

0 comments on commit 520caaf

Please sign in to comment.