blob: fd2d1580449d7899c85622d78f965bec52afb260 [file] [log] [blame]
Giorgi Lekveishvilie8b2f012023-11-30 19:05:03 +04001package controllers
2
3import (
Giorgi Lekveishvilie58fc592023-12-07 13:24:07 +04004 "encoding/json"
Giorgi Lekveishvilie8b2f012023-11-30 19:05:03 +04005 "fmt"
6 "io"
7 "io/fs"
8 "os"
Giorgi Lekveishvili1caed362023-12-13 16:29:43 +04009 "strings"
Giorgi Lekveishvilie8b2f012023-11-30 19:05:03 +040010 "text/template"
11
12 "github.com/Masterminds/sprig/v3"
13 "github.com/go-git/go-billy/v5"
14 "github.com/go-git/go-billy/v5/util"
15)
16
Giorgi Lekveishvilie58fc592023-12-07 13:24:07 +040017const dodoConfigFilename = "dodo.json"
Giorgi Lekveishvilie8b2f012023-11-30 19:05:03 +040018const zoneConfigFilename = "coredns.conf"
19const rootConfigFilename = "coredns.conf"
20const importAllConfigFiles = "import */" + zoneConfigFilename
21
22type ZoneStore interface {
23 ConfigPath() string
Giorgi Lekveishvilie58fc592023-12-07 13:24:07 +040024 CreateConfigFile() error
Giorgi Lekveishvilie8b2f012023-11-30 19:05:03 +040025 AddDNSSec(key DNSSecKey) error
Giorgi Lekveishvilie58fc592023-12-07 13:24:07 +040026 AddTextRecord(entry, txt string) error
27 DeleteTextRecord(entry, txt string) error
Giorgi Lekveishvilie8b2f012023-11-30 19:05:03 +040028}
29
30type ZoneConfig struct {
Giorgi Lekveishvilie58fc592023-12-07 13:24:07 +040031 Zone string `json:"zone,omitempty"`
32 PublicIPs []string `json:"publicIPs,omitempty"`
Giorgi Lekveishvili109a5672023-12-07 16:05:42 +040033 PrivateIP string `json:"privateIP,omitempty"`
Giorgi Lekveishvilie58fc592023-12-07 13:24:07 +040034 Nameservers []string `json:"nameservers,omitempty"`
35 DNSSec *DNSSecKey `json:"dnsSec,omitempty"`
Giorgi Lekveishvilie8b2f012023-11-30 19:05:03 +040036}
37
Giorgi Lekveishvili1caed362023-12-13 16:29:43 +040038func GenerateNSRecords(z ZoneConfig) []string {
39 subdomain := strings.Split(z.Zone, ",")[0]
40 ret := make([]string, 0)
41 for i, ip := range z.Nameservers {
42 ret = append(ret, fmt.Sprintf("ns%d.%s 10800 IN A %s", i+1, z.Zone, ip))
43 ret = append(ret, fmt.Sprintf("%s. 10800 IN NS ns%d.%s.", subdomain, i+1, z.Zone))
44 }
45 return ret
46}
47
Giorgi Lekveishvilie8b2f012023-11-30 19:05:03 +040048type ZoneStoreFactory interface {
49 ConfigPath() string
50 Create(zone ZoneConfig) (ZoneStore, error)
Giorgi Lekveishvilie58fc592023-12-07 13:24:07 +040051 Get(zone string) (ZoneStore, error)
Giorgi Lekveishvilie8b2f012023-11-30 19:05:03 +040052 Debug()
Giorgi Lekveishvilie58fc592023-12-07 13:24:07 +040053 Purge()
Giorgi Lekveishvilie8b2f012023-11-30 19:05:03 +040054}
55
56type fsZoneStoreFactory struct {
Giorgi Lekveishvilie58fc592023-12-07 13:24:07 +040057 fs billy.Filesystem
58 zones map[string]ZoneStore
Giorgi Lekveishvilie8b2f012023-11-30 19:05:03 +040059}
60
61func NewFSZoneStoreFactory(fs billy.Filesystem) (ZoneStoreFactory, error) {
62 if err := util.WriteFile(fs, rootConfigFilename, []byte(importAllConfigFiles), os.ModePerm); err != nil {
63 return nil, err
64 }
Giorgi Lekveishvilie58fc592023-12-07 13:24:07 +040065 f, err := fs.ReadDir(".")
66 if err != nil {
67 return nil, err
68 }
69 zf := fsZoneStoreFactory{fs: fs, zones: make(map[string]ZoneStore)}
70 for _, i := range f {
71 if i.IsDir() {
72 var zone ZoneConfig
73 r, err := fs.Open(fs.Join(i.Name(), dodoConfigFilename))
74 if err != nil {
75 continue // TODO(gio): clean up the dir to enforce config file
76 }
77 defer r.Close()
78 if err := json.NewDecoder(r).Decode(&zone); err != nil {
79 return nil, err
80 }
81 zfs, err := fs.Chroot(zone.Zone)
82 if err != nil {
83 return nil, err
84 }
85 z, err := NewFSZoneStore(zone, zfs)
86 zf.zones[zone.Zone] = z
87 }
88 }
89 return &zf, nil
Giorgi Lekveishvilie8b2f012023-11-30 19:05:03 +040090}
91
92func (f *fsZoneStoreFactory) ConfigPath() string {
93 return f.fs.Join(f.fs.Root(), rootConfigFilename)
94}
95
Giorgi Lekveishvilie58fc592023-12-07 13:24:07 +040096func (f *fsZoneStoreFactory) Purge() {
97 items, _ := f.fs.ReadDir(".")
98 for _, i := range items {
99 f.fs.Remove(i.Name())
100 }
101}
102
Giorgi Lekveishvilie8b2f012023-11-30 19:05:03 +0400103func (f *fsZoneStoreFactory) Debug() {
104 fmt.Println("------------")
105 util.Walk(f.fs, ".", func(path string, info fs.FileInfo, err error) error {
106 fmt.Println(path)
107 if !info.IsDir() {
108 r, err := f.fs.Open(path)
109 if err != nil {
110 return err
111 }
112 defer r.Close()
113 _, err = io.Copy(os.Stdout, r)
114 return err
115 }
116 return nil
117 })
118 fmt.Println("++++++++++++++")
119}
120
Giorgi Lekveishvilie58fc592023-12-07 13:24:07 +0400121func (f *fsZoneStoreFactory) Get(zone string) (ZoneStore, error) {
122 if z, ok := f.zones[zone]; ok {
123 return z, nil
124 }
125 return nil, fmt.Errorf("%s zone not found", zone)
126}
127
Giorgi Lekveishvilie8b2f012023-11-30 19:05:03 +0400128func (f *fsZoneStoreFactory) Create(zone ZoneConfig) (ZoneStore, error) {
Giorgi Lekveishvilie58fc592023-12-07 13:24:07 +0400129 if z, ok := f.zones[zone.Zone]; ok {
130 return z, nil
131 }
Giorgi Lekveishvilie8b2f012023-11-30 19:05:03 +0400132 if err := f.fs.MkdirAll(zone.Zone, fs.ModePerm); err != nil {
133 return nil, err
134 }
135 zfs, err := f.fs.Chroot(zone.Zone)
136 if err != nil {
137 return nil, err
138 }
139 z, err := NewFSZoneStore(zone, zfs)
140 if err != nil {
141 defer func() {
142 if err := f.fs.Remove(zone.Zone); err != nil {
143 fmt.Printf("Failed to remove zone directory: %s\n", err.Error())
144 }
145 }()
146 }
Giorgi Lekveishvilie58fc592023-12-07 13:24:07 +0400147 f.zones[zone.Zone] = z
Giorgi Lekveishvilie8b2f012023-11-30 19:05:03 +0400148 return z, nil
149}
150
151type fsZoneStore struct {
152 zone ZoneConfig
153 fs billy.Filesystem
154}
155
156func NewFSZoneStore(zone ZoneConfig, fs billy.Filesystem) (ZoneStore, error) {
Giorgi Lekveishvilie58fc592023-12-07 13:24:07 +0400157 return &fsZoneStore{zone, fs}, nil
158}
159
160func (s *fsZoneStore) CreateConfigFile() error {
161 {
162 w, err := s.fs.Create(dodoConfigFilename)
163 if err != nil {
164 return err
165 }
166 defer w.Close()
167 if err := json.NewEncoder(w).Encode(s.zone); err != nil {
168 return err
169 }
170 }
171 zone := s.zone
172 fs := s.fs
Giorgi Lekveishvilie8b2f012023-11-30 19:05:03 +0400173 if zone.DNSSec != nil {
174 sec := zone.DNSSec
175 if err := util.WriteFile(fs, sec.Basename+".key", sec.Key, 0644); err != nil {
Giorgi Lekveishvilie58fc592023-12-07 13:24:07 +0400176 return err
Giorgi Lekveishvilie8b2f012023-11-30 19:05:03 +0400177 }
178 if err := util.WriteFile(fs, sec.Basename+".private", sec.Private, 0600); err != nil {
Giorgi Lekveishvilie58fc592023-12-07 13:24:07 +0400179 return err
Giorgi Lekveishvilie8b2f012023-11-30 19:05:03 +0400180 }
181 }
182 conf, err := fs.Create(zoneConfigFilename)
183 if err != nil {
Giorgi Lekveishvilie58fc592023-12-07 13:24:07 +0400184 return err
Giorgi Lekveishvilie8b2f012023-11-30 19:05:03 +0400185 }
186 defer conf.Close()
187 configTmpl, err := template.New("config").Funcs(sprig.TxtFuncMap()).Parse(`
188{{ .zone.Zone }}:53 {
Giorgi Lekveishvilie58fc592023-12-07 13:24:07 +0400189 file {{ .rootDir }}/zone.db {
190 reload 1s
191 }
Giorgi Lekveishvilie8b2f012023-11-30 19:05:03 +0400192 errors
193 {{ if .zone.DNSSec }}
194 dnssec {
195 key file {{ .rootDir}}/{{ .zone.DNSSec.Basename }}
196 }
197 {{ end }}
198 log
199 health {
200 lameduck 5s
201 }
202 ready
203 cache 30
204 loop
205 reload
206 loadbalance
207}`)
208 if err != nil {
Giorgi Lekveishvilie58fc592023-12-07 13:24:07 +0400209 return err
Giorgi Lekveishvilie8b2f012023-11-30 19:05:03 +0400210 }
211 if err := configTmpl.Execute(conf, map[string]any{
212 "zone": zone,
213 "rootDir": fs.Root(),
214 }); err != nil {
Giorgi Lekveishvilie58fc592023-12-07 13:24:07 +0400215 return err
Giorgi Lekveishvilie8b2f012023-11-30 19:05:03 +0400216 }
217 recordsTmpl, err := template.New("records").Funcs(sprig.TxtFuncMap()).Parse(`
Giorgi Lekveishvilie58fc592023-12-07 13:24:07 +0400218{{ $zone := .zone }}
219{{ $zone }}. IN SOA ns1.{{ $zone }}. hostmaster.{{ $zone }}. {{ .nowUnix }} 7200 3600 1209600 3600
Giorgi Lekveishvilie8b2f012023-11-30 19:05:03 +0400220{{ range $i, $ns := .nameservers }}
Giorgi Lekveishvilie58fc592023-12-07 13:24:07 +0400221ns{{ add1 $i }}.{{ $zone }}. 10800 IN A {{ $ns }}
Giorgi Lekveishvilie8b2f012023-11-30 19:05:03 +0400222{{ end }}
223{{ range .publicIngressIPs }}
Giorgi Lekveishvili109a5672023-12-07 16:05:42 +0400224{{ $zone }}. 10800 IN A {{ . }}
225*.{{ $zone }}. 10800 IN A {{ . }}
226*.*.{{ $zone }}. 10800 IN A {{ . }}
Giorgi Lekveishvilie8b2f012023-11-30 19:05:03 +0400227{{ end }}
Giorgi Lekveishvilie58fc592023-12-07 13:24:07 +0400228*.p.{{ $zone }}. 10800 IN A {{ .privateIngressIP }}
Giorgi Lekveishvilie8b2f012023-11-30 19:05:03 +0400229`)
230 records, err := fs.Create("zone.db")
231 if err != nil {
Giorgi Lekveishvilie58fc592023-12-07 13:24:07 +0400232 return err
Giorgi Lekveishvilie8b2f012023-11-30 19:05:03 +0400233 }
234 defer records.Close()
235 if err := recordsTmpl.Execute(records, map[string]any{
236 "zone": zone.Zone,
237 "publicIngressIPs": zone.PublicIPs,
238 "privateIngressIP": zone.PrivateIP,
239 "nameservers": zone.Nameservers,
Giorgi Lekveishvilie58fc592023-12-07 13:24:07 +0400240 "nowUnix": NowUnix(),
Giorgi Lekveishvilie8b2f012023-11-30 19:05:03 +0400241 }); err != nil {
Giorgi Lekveishvilie58fc592023-12-07 13:24:07 +0400242 return err
Giorgi Lekveishvilie8b2f012023-11-30 19:05:03 +0400243 }
Giorgi Lekveishvilie58fc592023-12-07 13:24:07 +0400244 return nil
Giorgi Lekveishvilie8b2f012023-11-30 19:05:03 +0400245}
246
247func (s *fsZoneStore) ConfigPath() string {
248 return s.fs.Join(s.fs.Root(), zoneConfigFilename)
249}
250
251func (s *fsZoneStore) AddDNSSec(key DNSSecKey) error {
252 return nil
253}
Giorgi Lekveishvilie58fc592023-12-07 13:24:07 +0400254
255func (s *fsZoneStore) AddTextRecord(entry, txt string) error {
256 s.fs.Remove("txt")
257 r, err := s.fs.Open("zone.db")
258 if err != nil {
259 return err
260 }
261 defer r.Close()
262 z, err := NewZoneFile(r)
263 if err != nil {
264 return err
265 }
Giorgi Lekveishvili109a5672023-12-07 16:05:42 +0400266 var fqdn = fmt.Sprintf("%s.%s.", entry, s.zone.Zone)
267 z.CreateOrReplaceTxtRecord(fqdn, txt)
268 for _, ip := range s.zone.PublicIPs {
269 z.CreateARecord(fqdn, ip)
270 }
Giorgi Lekveishvilie58fc592023-12-07 13:24:07 +0400271 w, err := s.fs.Create("zone.db")
272 if err != nil {
273 return err
274 }
275 defer w.Close()
276 if err := z.Write(w); err != nil {
277 return err
278 }
Giorgi Lekveishvilie58fc592023-12-07 13:24:07 +0400279 return nil
280}
281
282func (s *fsZoneStore) DeleteTextRecord(entry, txt string) error {
283 r, err := s.fs.Open("zone.db")
284 if err != nil {
285 return err
286 }
287 defer r.Close()
288 z, err := NewZoneFile(r)
289 if err != nil {
290 return err
291 }
Giorgi Lekveishvili109a5672023-12-07 16:05:42 +0400292 fqdn := fmt.Sprintf("%s.%s.", entry, s.zone.Zone)
293 z.DeleteTxtRecord(fqdn, txt)
294 z.DeleteRecordsFor(fqdn)
Giorgi Lekveishvilie58fc592023-12-07 13:24:07 +0400295 w, err := s.fs.Create("zone.db")
296 if err != nil {
297 return err
298 }
299 defer w.Close()
300 if err := z.Write(w); err != nil {
301 return err
302 }
303 return nil
304}