Skip to content

Commit 3a4dc81

Browse files
oliwerGopher Bot
authored andcommitted
MINOR: acme: add support for acme-provider and acme-vars
Those 2 new keywords are used to configure how to solve a dns-01 challenge. For example: acme-provider godaddy acme-vars "ApiToken=XXXX" To find which variables are needed for each provider, look at the Provider's struct definition in github.com/libdns.
1 parent 48a91d6 commit 3a4dc81

File tree

12 files changed

+257
-25
lines changed

12 files changed

+257
-25
lines changed

.aspell.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ allowed:
6666
- fullpage
6767
- github
6868
- gitlab
69+
- godaddy
6970
- gokc
7071
- golang
7172
- golangci
@@ -188,3 +189,4 @@ allowed:
188189
- yaml
189190
- async
190191
- rehaul
192+
- XXXX

config-parser/section-parsers.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1035,6 +1035,8 @@ func (p *configParser) getAcmeParser() *Parsers {
10351035
parser := map[string]ParserInterface{}
10361036
sequence := []Section{}
10371037
addParser(parser, &sequence, &simple.Word{Name: "account-key"})
1038+
addParser(parser, &sequence, &simple.Word{Name: "acme-provider"})
1039+
addParser(parser, &sequence, &simple.Word{Name: "acme-vars"})
10381040
addParser(parser, &sequence, &simple.Number{Name: "bits"})
10391041
addParser(parser, &sequence, &simple.Word{Name: "challenge"})
10401042
addParser(parser, &sequence, &simple.Word{Name: "contact"})

configuration/acme_provider.go

Lines changed: 125 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package configuration
1818
import (
1919
"errors"
2020
"fmt"
21+
"strings"
2122

2223
strfmt "github.com/go-openapi/strfmt"
2324
parser "github.com/haproxytech/client-native/v6/config-parser"
@@ -159,14 +160,18 @@ func ParseAcmeProvider(p parser.Parser, name string) (*models.AcmeProvider, erro
159160
}
160161
}
161162

163+
var varsStr string
164+
162165
stringAttr := map[string]*string{
163-
"account-key": &acme.AccountKey,
164-
"challenge": &acme.Challenge,
165-
"contact": &acme.Contact,
166-
"curves": &acme.Curves,
167-
"directory": &acme.Directory,
168-
"keytype": &acme.Keytype,
169-
"map": &acme.Map,
166+
"account-key": &acme.AccountKey,
167+
"acme-provider": &acme.AcmeProvider,
168+
"acme-vars": &varsStr,
169+
"challenge": &acme.Challenge,
170+
"contact": &acme.Contact,
171+
"curves": &acme.Curves,
172+
"directory": &acme.Directory,
173+
"keytype": &acme.Keytype,
174+
"map": &acme.Map,
170175
}
171176

172177
for kw, dest := range stringAttr {
@@ -198,6 +203,9 @@ func ParseAcmeProvider(p parser.Parser, name string) (*models.AcmeProvider, erro
198203
acme.Bits = misc.Ptr(ic.Value)
199204
}
200205

206+
// acme-vars
207+
acme.AcmeVars = parseAcmeVars(varsStr)
208+
201209
return acme, nil
202210
}
203211

@@ -216,14 +224,21 @@ func SerializeAcmeProvider(p parser.Parser, acme *models.AcmeProvider) error {
216224
}
217225
}
218226

227+
acmeVars, err := serializeAcmeVars(acme.AcmeVars)
228+
if err != nil {
229+
return fmt.Errorf("acme %s: %w", acme.Name, err)
230+
}
231+
219232
stringAttr := map[string]string{
220-
"account-key": acme.AccountKey,
221-
"challenge": acme.Challenge,
222-
"contact": acme.Contact,
223-
"curves": acme.Curves,
224-
"directory": acme.Directory,
225-
"keytype": acme.Keytype,
226-
"map": acme.Map,
233+
"account-key": acme.AccountKey,
234+
"acme-provider": acme.AcmeProvider,
235+
"acme-vars": acmeVars,
236+
"challenge": acme.Challenge,
237+
"contact": acme.Contact,
238+
"curves": acme.Curves,
239+
"directory": acme.Directory,
240+
"keytype": acme.Keytype,
241+
"map": acme.Map,
227242
}
228243

229244
for kw, val := range stringAttr {
@@ -246,3 +261,99 @@ func SerializeAcmeProvider(p parser.Parser, acme *models.AcmeProvider) error {
246261

247262
return nil
248263
}
264+
265+
// acme-vars "key=value,foo=\"bar baz\""
266+
func serializeAcmeVars(vars map[string]string) (string, error) {
267+
if len(vars) == 0 {
268+
return "", nil
269+
}
270+
271+
var sb strings.Builder
272+
first := true
273+
274+
sb.WriteByte('"')
275+
for k, v := range vars {
276+
if len(k) == 0 {
277+
continue
278+
}
279+
if !acmeValidKey(k) {
280+
return "", fmt.Errorf("acme-vars: invalid character found in key '%s'", k)
281+
}
282+
if first {
283+
first = false
284+
} else {
285+
sb.WriteByte(',')
286+
}
287+
sb.WriteString(k)
288+
sb.WriteByte('=')
289+
sb.WriteString(acmeVarEscape(v))
290+
}
291+
sb.WriteByte('"')
292+
293+
return sb.String(), nil
294+
}
295+
296+
func parseAcmeVars(vars string) map[string]string {
297+
n := len(vars)
298+
if n == 0 {
299+
return nil
300+
}
301+
302+
if vars[0] == '"' && vars[n-1] == '"' {
303+
vars = vars[1 : n-1]
304+
}
305+
306+
vars = strings.TrimSpace(vars)
307+
if len(vars) == 0 {
308+
return nil
309+
}
310+
311+
vlist := acmeVarSplit(vars)
312+
vmap := make(map[string]string, len(vlist))
313+
for _, keyval := range vlist {
314+
if k, v, found := strings.Cut(strings.TrimSpace(keyval), "="); found {
315+
if len(k) > 0 {
316+
vmap[k] = acmeVarUnescape(v)
317+
}
318+
}
319+
}
320+
321+
if len(vmap) == 0 {
322+
return nil
323+
}
324+
return vmap
325+
}
326+
327+
// Split string by ',' but not escaped commas "\,".
328+
func acmeVarSplit(s string) []string {
329+
s = strings.ReplaceAll(s, `\,`, "\x00")
330+
tokens := strings.Split(s, ",")
331+
for i, token := range tokens {
332+
tokens[i] = strings.ReplaceAll(token, "\x00", `\,`)
333+
}
334+
return tokens
335+
}
336+
337+
func acmeVarEscape(s string) string {
338+
s = strings.ReplaceAll(s, `"`, `\"`)
339+
s = strings.ReplaceAll(s, `,`, `\,`)
340+
return s
341+
}
342+
343+
func acmeVarUnescape(s string) string {
344+
s = strings.ReplaceAll(s, `\"`, `"`)
345+
s = strings.ReplaceAll(s, `\,`, `,`)
346+
return s
347+
}
348+
349+
// Variable keys must also be valid Go variable names.
350+
func acmeValidKey(key string) bool {
351+
for _, c := range key {
352+
match := ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z') ||
353+
('0' <= c && c <= '9') || c == '_'
354+
if !match {
355+
return false
356+
}
357+
}
358+
return true
359+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Copyright 2025 HAProxy Technologies
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
//
15+
16+
package configuration
17+
18+
import (
19+
"testing"
20+
21+
"github.com/google/go-cmp/cmp"
22+
)
23+
24+
func Test_serializeAcmeVars(t *testing.T) {
25+
tests := []struct {
26+
vars map[string]string
27+
want string
28+
wantErr bool
29+
}{
30+
{
31+
vars: map[string]string{"foo": "bar", "ApiKey": "FEFF,==\""},
32+
want: `"foo=bar,ApiKey=FEFF\,==\""`,
33+
wantErr: false,
34+
},
35+
}
36+
for _, tt := range tests {
37+
t.Run(tt.want, func(t *testing.T) {
38+
got, err := serializeAcmeVars(tt.vars)
39+
if tt.wantErr != (err != nil) {
40+
t.Errorf("serializeAcmeVars() got error '%v', wantErr=%v", err, tt.wantErr)
41+
}
42+
if got != tt.want {
43+
t.Errorf("serializeAcmeVars() = %v, want %v", got, tt.want)
44+
}
45+
})
46+
}
47+
}
48+
49+
func Test_parseAcmeVars(t *testing.T) {
50+
tests := []struct {
51+
vars string
52+
want map[string]string
53+
}{
54+
{
55+
vars: `"foo=bar,ApiKey=FEFF\,==\""`,
56+
want: map[string]string{"foo": "bar", "ApiKey": "FEFF,==\""},
57+
},
58+
}
59+
for _, tt := range tests {
60+
t.Run(tt.vars, func(t *testing.T) {
61+
got := parseAcmeVars(tt.vars)
62+
if !cmp.Equal(got, tt.want) {
63+
t.Errorf("parseAcmeVars() = %#v, want %#v", got, tt.want)
64+
}
65+
})
66+
}
67+
}

models/acme_provider.go

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

models/acme_provider_compare.go

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

models/acme_provider_compare_test.go

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

specification/build/haproxy_spec.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11877,11 +11877,19 @@ definitions:
1187711877
x-omitempty: true
1187811878
x-go-name: SSLFrontUses
1187911879
acme_provider:
11880+
additionalProperties: false
1188011881
description: Define an ACME provider to generate certificates automatically
1188111882
properties:
1188211883
account_key:
1188311884
description: Path where the the ACME account key is stored
1188411885
type: string
11886+
acme_provider:
11887+
description: DNS provider for the dns-01 challenge
11888+
type: string
11889+
acme_vars:
11890+
additionalProperties:
11891+
type: string
11892+
description: List of variables passed to the dns-01 provider (typically API keys)
1188511893
bits:
1188611894
description: Number of bits to generate an RSA certificate
1188711895
minimum: 1024

specification/models/configuration/acme.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@ acme:
1414
account_key:
1515
type: string
1616
description: Path where the the ACME account key is stored
17+
acme_provider:
18+
type: string
19+
description: DNS provider for the dns-01 challenge
20+
acme_vars:
21+
description: List of variables passed to the dns-01 provider (typically API keys)
22+
additionalProperties:
23+
type: string
1724
bits:
1825
type: integer
1926
description: Number of bits to generate an RSA certificate
@@ -47,3 +54,4 @@ acme:
4754
metadata:
4855
additionalProperties:
4956
type: object
57+
additionalProperties: false

test/acme_test.go

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -94,15 +94,20 @@ func TestCreateEditDeleteAcmeProvider(t *testing.T) {
9494
require := require.New(t)
9595

9696
a := &models.AcmeProvider{
97-
Name: "ninja",
98-
AccountKey: "ninja-acme.key",
99-
Bits: misc.Int64P(2048),
100-
Challenge: "http-01",
101-
Contact: "me@example.com",
102-
Curves: "dem curves",
103-
Directory: "https://acme.ninja.com/directory",
104-
Keytype: "ECDSA",
105-
Map: "acme@virt",
97+
Name: "ninja",
98+
AccountKey: "ninja-acme.key",
99+
AcmeProvider: "godaddy",
100+
AcmeVars: map[string]string{
101+
"ApiKey": "foobar",
102+
"WeirdKey": "\"__, +=\"",
103+
},
104+
Bits: misc.Int64P(2048),
105+
Challenge: "http-01",
106+
Contact: "me@example.com",
107+
Curves: "dem curves",
108+
Directory: "https://acme.ninja.com/directory",
109+
Keytype: "ECDSA",
110+
Map: "acme@virt",
106111
}
107112

108113
err := clientTest.CreateAcmeProvider(a, "", version)

0 commit comments

Comments
 (0)