317 lines
7.8 kB
1
package main
2
3
import (
4
"embed"
5
"encoding/json"
6
"errors"
7
"flag"
8
"fmt"
9
"io"
10
"log"
11
"os"
12
13
"github.com/yokecd/yoke/pkg/flight"
14
externaldns "go.techaro.lol/hypercloud/helm/external-dns"
15
"k8s.io/apimachinery/pkg/util/yaml"
16
17
acmev1 "github.com/cert-manager/cert-manager/pkg/apis/acme/v1"
18
certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
19
certmanagermetav1 "github.com/cert-manager/cert-manager/pkg/apis/meta/v1"
20
corev1 "k8s.io/api/core/v1"
21
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
22
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
23
)
24
25
type Config struct {
26
ACME *ACME `json:"acme"`
27
ExternalDNS map[string]any `json:"externalDNS"`
28
ExternalIP IP `json:"externalIP"`
29
}
30
31
type IP struct {
32
IPv4 *string `json:"ipv4,omitempty"`
33
IPv6 *string `json:"ipv6,omitempty"`
34
}
35
36
func (ip IP) Valid() error {
37
var errs []error
38
if ip.IPv4 == nil && ip.IPv6 == nil {
39
errs = append(errs, fmt.Errorf("ipv4 or ipv6 is required"))
40
}
41
if len(errs) > 0 {
42
return fmt.Errorf("ip is invalid: %v", errors.Join(errs...))
43
}
44
45
return nil
46
}
47
48
func (c Config) Valid() error {
49
var errs []error
50
if c.ACME == nil {
51
errs = append(errs, fmt.Errorf("acme is required"))
52
} else {
53
if err := c.ACME.Valid(); err != nil {
54
errs = append(errs, fmt.Errorf("acme is invalid: %w", err))
55
}
56
}
57
if c.ExternalDNS == nil {
58
errs = append(errs, fmt.Errorf("externalDNS is required"))
59
}
60
if c.ExternalDNS["extraArgs"] == nil {
61
errs = append(errs, fmt.Errorf("externalDNS.extraArgs is required"))
62
}
63
if _, ok := c.ExternalDNS["extraArgs"].([]any); !ok {
64
errs = append(errs, fmt.Errorf("externalDNS.extraArgs must be a list of strings, it is %T", c.ExternalDNS["extraArgs"]))
65
}
66
if err := c.ExternalIP.Valid(); err != nil {
67
errs = append(errs, fmt.Errorf("externalIP is invalid: %w", err))
68
}
69
if len(errs) > 0 {
70
return fmt.Errorf("config is invalid: %v", errors.Join(errs...))
71
}
72
73
return nil
74
}
75
76
type ACME struct {
77
Email string `json:"email"`
78
Directories []ACMEDirectory `json:"directories"`
79
Solvers []acmev1.ACMEChallengeSolver `json:"solvers"`
80
}
81
82
func (acme ACME) Valid() error {
83
var errs []error
84
if acme.Email == "" {
85
errs = append(errs, fmt.Errorf("email is required"))
86
}
87
if len(acme.Directories) == 0 {
88
errs = append(errs, fmt.Errorf("directories are required"))
89
}
90
for _, directory := range acme.Directories {
91
if err := directory.Valid(); err != nil {
92
errs = append(errs, fmt.Errorf("directory %s is invalid: %w", directory.Name, err))
93
}
94
}
95
96
if len(errs) > 0 {
97
return fmt.Errorf("acme is invalid: %v", errors.Join(errs...))
98
}
99
100
return nil
101
}
102
103
type ACMEDirectory struct {
104
URL string `json:"url"`
105
Name string `json:"name"`
106
}
107
108
func (ad ACMEDirectory) Valid() error {
109
var errs []error
110
if ad.URL == "" {
111
errs = append(errs, fmt.Errorf("url is required"))
112
}
113
if ad.Name == "" {
114
errs = append(errs, fmt.Errorf("name is required"))
115
}
116
if len(errs) > 0 {
117
return fmt.Errorf("acme directory is invalid: %v", errors.Join(errs...))
118
}
119
120
return nil
121
}
122
123
//go:embed data/*.yaml
124
var data embed.FS
125
126
func main() {
127
flag.Parse()
128
if err := run(); err != nil {
129
log.Fatal(err)
130
}
131
}
132
133
func run() error {
134
var cfg Config
135
fin, err := data.Open("data/default-config.yaml")
136
if err != nil {
137
return fmt.Errorf("failed to open default-config.yaml: %w", err)
138
}
139
defer fin.Close()
140
141
if err := yaml.NewYAMLToJSONDecoder(fin).Decode(&cfg); err != nil {
142
return fmt.Errorf("failed to decode default-config.yaml: %w", err)
143
}
144
145
if err := yaml.NewYAMLToJSONDecoder(os.Stdin).Decode(&cfg); err != nil && err != io.EOF {
146
return fmt.Errorf("failed to decode stdin: %w", err)
147
}
148
149
if err := cfg.Valid(); err != nil {
150
return fmt.Errorf("config is invalid: %w", err)
151
}
152
153
var result []any
154
155
result = append(result, []any{corev1.Namespace{
156
TypeMeta: metav1.TypeMeta{
157
APIVersion: "v1",
158
Kind: "Namespace",
159
},
160
ObjectMeta: metav1.ObjectMeta{
161
Name: "tor-controller-system",
162
},
163
}})
164
165
fin, err = data.Open("data/tor-controller.yaml")
166
if err != nil {
167
return fmt.Errorf("failed to open tor-controller.yaml: %w", err)
168
}
169
defer fin.Close()
170
171
torController, err := readEveryDocument(fin)
172
if err != nil {
173
return fmt.Errorf("failed to read tor-controller.yaml: %w", err)
174
}
175
176
result = append(result, torController)
177
178
result = append(result, []any{corev1.Namespace{
179
TypeMeta: metav1.TypeMeta{
180
APIVersion: "v1",
181
Kind: "Namespace",
182
},
183
ObjectMeta: metav1.ObjectMeta{
184
Name: "cert-manager",
185
},
186
}})
187
188
fin, err = data.Open("data/cert-manager.yaml")
189
if err != nil {
190
return fmt.Errorf("failed to open cert-manager.yaml: %w", err)
191
}
192
defer fin.Close()
193
194
certManager, err := readEveryDocument(fin)
195
if err != nil {
196
return fmt.Errorf("failed to read cert-manager.yaml: %w", err)
197
}
198
199
result = append(result, certManager)
200
201
var directories []any
202
203
for _, directory := range cfg.ACME.Directories {
204
directories = append(directories, makeClusterIssuer(cfg.ACME, directory))
205
}
206
207
result = append(result, directories)
208
209
fin, err = data.Open("data/external-dns-crd.yaml")
210
if err != nil {
211
return fmt.Errorf("failed to open external-dns-crd.yaml: %w", err)
212
}
213
defer fin.Close()
214
215
extDNSCRD, err := readEveryDocument(fin)
216
if err != nil {
217
return fmt.Errorf("failed to read external-dns-crd.yaml: %w", err)
218
}
219
220
result = append(result, extDNSCRD)
221
222
result = append(result, []any{corev1.Namespace{
223
TypeMeta: metav1.TypeMeta{
224
APIVersion: "v1",
225
Kind: "Namespace",
226
},
227
ObjectMeta: metav1.ObjectMeta{
228
Name: "external-dns",
229
},
230
}})
231
232
extraArgs, ok := cfg.ExternalDNS["extraArgs"].([]any)
233
if !ok {
234
return fmt.Errorf("externalDNS.extraArgs must be a list of something")
235
}
236
237
for _, recordType := range []string{"A", "AAAA", "CNAME", "TXT"} {
238
extraArgs = append(extraArgs, "--managed-record-types="+recordType)
239
}
240
241
if cfg.ExternalIP.IPv4 != nil {
242
extraArgs = append(extraArgs, "--default-targets="+*cfg.ExternalIP.IPv4)
243
}
244
if cfg.ExternalIP.IPv6 != nil {
245
extraArgs = append(extraArgs, "--default-targets="+*cfg.ExternalIP.IPv6)
246
}
247
248
cfg.ExternalDNS["extraArgs"] = extraArgs
249
250
externalDNS, err := externaldns.RenderChart(flight.Release(), "external-dns", cfg.ExternalDNS)
251
if err != nil {
252
return fmt.Errorf("failed to render external-dns chart: %w", err)
253
}
254
255
// Filter out PodDisruptionBudgets from externalDNS
256
var filteredExternalDNS []*unstructured.Unstructured
257
for _, obj := range externalDNS {
258
if obj.GetKind() == "PodDisruptionBudget" {
259
// Skip PodDisruptionBudgets
260
continue
261
}
262
filteredExternalDNS = append(filteredExternalDNS, obj)
263
}
264
265
result = append(result, filteredExternalDNS)
266
267
return json.NewEncoder(os.Stdout).Encode(result)
268
}
269
270
func makeClusterIssuer(acme *ACME, directory ACMEDirectory) any {
271
return certmanagerv1.ClusterIssuer{
272
TypeMeta: metav1.TypeMeta{
273
APIVersion: certmanagerv1.SchemeGroupVersion.Identifier(),
274
Kind: "ClusterIssuer",
275
},
276
ObjectMeta: metav1.ObjectMeta{
277
Name: directory.Name,
278
},
279
Spec: certmanagerv1.IssuerSpec{
280
IssuerConfig: certmanagerv1.IssuerConfig{
281
ACME: &acmev1.ACMEIssuer{
282
Server: directory.URL,
283
Email: acme.Email,
284
PrivateKey: certmanagermetav1.SecretKeySelector{
285
LocalObjectReference: certmanagermetav1.LocalObjectReference{
286
Name: directory.Name + "-private-key",
287
},
288
},
289
Solvers: acme.Solvers,
290
},
291
},
292
},
293
}
294
}
295
296
func readEveryDocument(r io.Reader) ([]unstructured.Unstructured, error) {
297
var result []unstructured.Unstructured
298
299
dec := yaml.NewYAMLToJSONDecoder(r)
300
for {
301
var doc unstructured.Unstructured
302
if err := dec.Decode(&doc); err != nil {
303
if err == io.EOF {
304
break
305
}
306
return nil, err
307
}
308
309
if doc.GetAPIVersion() == "" {
310
continue
311
}
312
313
result = append(result, doc)
314
}
315
316
return result, nil
317
}
318