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