this repo has no description
at main 318 lines 7.8 kB view raw
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}