this repo has no description
1package main
2
3import (
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
26type 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
33type IP struct {
34 IPv4 *string `json:"ipv4,omitempty"`
35 IPv6 *string `json:"ipv6,omitempty"`
36}
37
38func (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 errors.Join(errs...)
45 }
46
47 return nil
48}
49
50func (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 errors.Join(errs...)
73 }
74
75 return nil
76}
77
78type ACME struct {
79 Email string `json:"email"`
80 Directories []ACMEDirectory `json:"directories"`
81 Solvers []acmev1.ACMEChallengeSolver `json:"solvers"`
82}
83
84func (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 errors.Join(errs...)
100 }
101
102 return nil
103}
104
105type ACMEDirectory struct {
106 URL string `json:"url"`
107 Name string `json:"name"`
108}
109
110func (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 errors.Join(errs...)
120 }
121
122 return nil
123}
124
125//go:embed data/*.yaml
126var data embed.FS
127
128func main() {
129 flag.Parse()
130 if err := run(); err != nil {
131 log.Fatal(err)
132 }
133}
134
135func 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
271func 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
297func 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}