1package main 2 3//go:generate go run . 4 5import ( 6 "bufio" 7 "bytes" 8 "errors" 9 "fmt" 10 "go/format" 11 "io" 12 "log" 13 "os" 14 "path/filepath" 15 "sort" 16 "strings" 17 "text/template" 18 19 "github.com/BurntSushi/toml" 20) 21 22const ( 23 root = "../../" 24 dnsPackage = root + "providers/dns" 25 mdTemplate = root + "internal/dnsdocs/dns.md.tmpl" 26 cliTemplate = root + "internal/dnsdocs/dns.go.tmpl" 27 cliOutput = root + "cmd/zz_gen_cmd_dnshelp.go" 28 docOutput = root + "docs/content/dns" 29 readmePath = root + "README.md" 30) 31 32const ( 33 startLine = "<!-- START DNS PROVIDERS LIST -->" 34 endLine = "<!-- END DNS PROVIDERS LIST -->" 35) 36 37type Model struct { 38 Name string // Real name of the DNS provider 39 Code string // DNS code 40 Since string // First lego version 41 URL string // DNS provider URL 42 Description string // Provider summary 43 Example string // CLI example 44 Configuration *Configuration // Environment variables 45 Links *Links // Links 46 Additional string // Extra documentation 47 GeneratedFrom string // Source file 48} 49 50type Configuration struct { 51 Credentials map[string]string 52 Additional map[string]string 53} 54 55type Links struct { 56 API string 57 GoClient string 58} 59 60type Providers struct { 61 Providers []Model 62} 63 64func main() { 65 models := &Providers{} 66 67 err := filepath.Walk(dnsPackage, walker(models)) 68 if err != nil { 69 log.Fatal(err) 70 } 71 72 // generate CLI help 73 err = generateCLIHelp(models) 74 if err != nil { 75 log.Fatal(err) 76 } 77 78 // generate README.md 79 err = generateReadMe(models) 80 if err != nil { 81 log.Fatal(err) 82 } 83 84 fmt.Printf("Documentation for %d DNS providers has been generated.\n", len(models.Providers)+1) 85} 86 87func walker(prs *Providers) func(string, os.FileInfo, error) error { 88 return func(path string, _ os.FileInfo, err error) error { 89 if err != nil { 90 return err 91 } 92 93 if filepath.Ext(path) == ".toml" { 94 m := Model{} 95 96 m.GeneratedFrom, err = filepath.Rel(root, path) 97 if err != nil { 98 return err 99 } 100 101 _, err := toml.DecodeFile(path, &m) 102 if err != nil { 103 return err 104 } 105 106 prs.Providers = append(prs.Providers, m) 107 108 // generate documentation 109 return generateDocumentation(m) 110 } 111 112 return nil 113 } 114} 115 116func generateDocumentation(m Model) error { 117 filename := filepath.Join(docOutput, "zz_gen_"+m.Code+".md") 118 119 file, err := os.Create(filename) 120 if err != nil { 121 return err 122 } 123 124 return template.Must(template.ParseFiles(mdTemplate)).Execute(file, m) 125} 126 127func generateCLIHelp(models *Providers) error { 128 filename := filepath.Join(cliOutput) 129 130 file, err := os.Create(filename) 131 if err != nil { 132 return err 133 } 134 135 tlt := template.New(filepath.Base(cliTemplate)).Funcs(map[string]interface{}{ 136 "safe": func(src string) string { 137 return strings.ReplaceAll(src, "`", "'") 138 }, 139 }) 140 141 b := &bytes.Buffer{} 142 err = template.Must(tlt.ParseFiles(cliTemplate)).Execute(b, models) 143 if err != nil { 144 return err 145 } 146 147 // gofmt 148 source, err := format.Source(b.Bytes()) 149 if err != nil { 150 return err 151 } 152 153 _, err = file.Write(source) 154 return err 155} 156 157func generateReadMe(models *Providers) error { 158 max, lines := extractTableData(models) 159 160 file, err := os.Open(readmePath) 161 if err != nil { 162 return err 163 } 164 165 defer func() { _ = file.Close() }() 166 167 var skip bool 168 169 buffer := bytes.NewBufferString("") 170 171 fileScanner := bufio.NewScanner(file) 172 for fileScanner.Scan() { 173 text := fileScanner.Text() 174 175 if text == startLine { 176 _, _ = fmt.Fprintln(buffer, text) 177 err = writeDNSTable(buffer, lines, max) 178 if err != nil { 179 return err 180 } 181 skip = true 182 } 183 184 if text == endLine { 185 skip = false 186 } 187 188 if skip { 189 continue 190 } 191 192 _, _ = fmt.Fprintln(buffer, text) 193 } 194 195 if fileScanner.Err() != nil { 196 return fileScanner.Err() 197 } 198 199 if skip { 200 return errors.New("missing end tag") 201 } 202 203 return os.WriteFile(readmePath, buffer.Bytes(), 0o666) 204} 205 206func extractTableData(models *Providers) (int, [][]string) { 207 readmePattern := "[%s](https://go-acme.github.io/lego/dns/%s/)" 208 209 items := []string{fmt.Sprintf(readmePattern, "Manual", "manual")} 210 211 var max int 212 213 for _, pvd := range models.Providers { 214 item := fmt.Sprintf(readmePattern, strings.ReplaceAll(pvd.Name, "|", "/"), pvd.Code) 215 items = append(items, item) 216 217 if max < len(item) { 218 max = len(item) 219 } 220 } 221 222 const nbCol = 4 223 224 sort.Slice(items, func(i, j int) bool { 225 return strings.ToLower(items[i]) < strings.ToLower(items[j]) 226 }) 227 228 var lines [][]string 229 var line []string 230 231 for i, item := range items { 232 switch { 233 case len(line) == nbCol: 234 lines = append(lines, line) 235 line = []string{item} 236 237 case i == len(items)-1: 238 line = append(line, item) 239 for j := len(line); j < nbCol; j++ { 240 line = append(line, "") 241 } 242 lines = append(lines, line) 243 244 default: 245 line = append(line, item) 246 } 247 } 248 249 if len(line) < nbCol { 250 for j := len(line); j < nbCol; j++ { 251 line = append(line, "") 252 } 253 lines = append(lines, line) 254 } 255 256 return max, lines 257} 258 259func writeDNSTable(w io.Writer, lines [][]string, size int) error { 260 _, err := fmt.Fprintf(w, "\n") 261 if err != nil { 262 return err 263 } 264 265 _, err = fmt.Fprintf(w, "|%[1]s|%[1]s|%[1]s|%[1]s|\n", strings.Repeat(" ", size+2)) 266 if err != nil { 267 return err 268 } 269 270 _, err = fmt.Fprintf(w, "|%[1]s|%[1]s|%[1]s|%[1]s|\n", strings.Repeat("-", size+2)) 271 if err != nil { 272 return err 273 } 274 275 linePattern := fmt.Sprintf("| %%-%[1]ds | %%-%[1]ds | %%-%[1]ds | %%-%[1]ds |\n", size) 276 for _, line := range lines { 277 _, err = fmt.Fprintf(w, linePattern, line[0], line[1], line[2], line[3]) 278 if err != nil { 279 return err 280 } 281 } 282 283 _, err = fmt.Fprintf(w, "\n") 284 return err 285} 286