package main import ( "bufio" "fmt" "log" "os" "sort" "strings" "sync" "time" "forgejo.edf-bootstrap.cx.fg1.ffm.osc.live/richardrobertreitz/docudile/lib" ) type field struct { name string enums string kind string required bool description []string fields []field uuid string } type spec struct { uuid string group string kind string version string field []string description []string fields []field enums string specs []spec } type index struct { name string version string key string uuid string group string } func main() { if len(os.Args) != 2 { log.Fatal("args: # output directory for generated files") } cluster, err := lib.ExecNotFatal(`#!/bin/bash kubectl config current-context `, []string{}) if err != nil { log.Fatal("error: ", err) } t := time.Now() cluster = ", " + cluster + ", " + t.String() outputDir := os.Args[1] if !strings.HasSuffix(outputDir, "/") { outputDir += "/" } create(false, "", outputDir, cluster) //create(true, "pod", outputDir) //create(true, "bashes.provisioning.shell.crossplane.io", outputDir) //return out, err := lib.ExecNotFatal(`#!/bin/bash kubectl api-resources --verbs=list -o name | sort `, []string{}) if err != nil { log.Fatal("error: ", err) } scans := []string{} scanner := bufio.NewScanner(strings.NewReader(out)) for scanner.Scan() { scans = append(scans, scanner.Text()) } if err := scanner.Err(); err != nil { log.Fatal("error: ", err) } var wg sync.WaitGroup wg.Add(len(scans)) for _, page := range scans { go func(page string) { defer wg.Done() if !strings.HasPrefix(page, "events.events.k8s.io") && !strings.HasPrefix(page, "nodes.metrics.k8s.io") && !strings.HasPrefix(page, "pods.metrics.k8s.io") { create(true, page, outputDir, cluster) fmt.Println("scaned " + page) } else { fmt.Println("warning, skipped " + page) } }(page) } wg.Wait() } func create(pages bool, page string, dir string, cluster string) { specs := make(map[string]spec) arg := "" if !pages { out, err := lib.ExecNotFatal(`#!/bin/bash kubectl api-resources --verbs=list -o name | sort `, []string{}) if err != nil { log.Fatal("error: ", err) } scanner := bufio.NewScanner(strings.NewReader(out)) for scanner.Scan() { spec := getSpec(0, scanner.Text()) if len(spec.uuid) > 0 { specs[spec.uuid] = spec } } if err := scanner.Err(); err != nil { log.Fatal("error: ", err) } } else { arg = page spec := getSpec(1, page) specs[spec.uuid] = spec } tree := []spec{} for _, spec := range specs { tree = append(tree, spec) } sort.SliceStable(tree, func(i, j int) bool { if tree[i].group == tree[j].group { return tree[i].kind < tree[j].kind } else { groupA := strings.Fields(strings.Replace(tree[i].group, ".", " ", -1)) revGroupA := strings.Join(reverse(groupA), ".") groupB := strings.Fields(strings.Replace(tree[j].group, ".", " ", -1)) revGroupB := strings.Join(reverse(groupB), ".") return revGroupA < revGroupB } }) if !pages { fmt.Println("create index") out := createFile(dir + "index.html") outOne := createFile(dir + "onepage.html") writeFile(out, "multi page one page"+cluster) writeFile(outOne, "multi page one page"+cluster) writeFile(out, "

Groups

") writeFile(outOne, "

Groups

") last := "" for _, object := range tree { if len(object.group) == 0 { if last != "default" { last = "default" writeFile(out, last+"
") writeFile(outOne, last+"
") } writeFile(out, ""+object.kind+"
") writeFile(outOne, ""+object.kind+"
") } else { group := strings.Fields(strings.Replace(object.group, ".", " ", -1)) revGroup := strings.Join(reverse(group), ".") if last != revGroup { last = revGroup writeFile(out, "
"+revGroup+"
") writeFile(outOne, "
"+revGroup+"
") } writeFile(out, ""+object.kind+"
") writeFile(outOne, ""+object.kind+"
") } } writeFile(out, "
") writeFile(outOne, "
") objects := []index{} for name, spec := range specs { objects = append(objects, index{spec.kind, spec.version, name, spec.uuid, spec.group}) } sort.SliceStable(objects, func(i, j int) bool { groupA := strings.Fields(strings.Replace(objects[i].group, ".", " ", -1)) revGroupA := strings.Join(reverse(groupA), ".") groupB := strings.Fields(strings.Replace(objects[j].group, ".", " ", -1)) revGroupB := strings.Join(reverse(groupB), ".") if objects[i].name == objects[j].name && revGroupA == revGroupB { return objects[i].version < objects[j].version } else if objects[i].name == objects[j].name { return revGroupA < revGroupB } else { return objects[i].name < objects[j].name } }) writeFile(out, "

Index

") writeFile(outOne, "

Index

") for _, object := range objects { if len(object.group) == 0 { writeFile(out, ""+object.name+" default
") writeFile(outOne, ""+object.name+" default
") } else { group := strings.Fields(strings.Replace(object.group, ".", " ", -1)) revGroup := strings.Join(reverse(group), ".") writeFile(out, ""+object.name+" "+revGroup+"
") writeFile(outOne, ""+object.name+" "+revGroup+"
") } } writeFile(out, "

") writeFile(outOne, "

") closeFile(out) closeFile(outOne) } else { for _, spec := range tree { printSpec(spec, true, arg, dir) } for _, spec := range tree { recurseSpecs(spec, arg, dir) } } } func getSpec(level int, specName string) spec { spec := spec{} if strings.HasPrefix(specName, "customresourcedefinitions.apiextensions.k8s.io.spec.versions.schema.openAPIV3Schema") { fmt.Println("warning, skipping " + specName) return spec } out, err := lib.ExecNotFatal(`#!/bin/bash kubectl explain "${SPEC}" `, []string{"SPEC=" + specName}) if err != nil { if specName == "events.events.k8s.io" || specName == "nodes.metrics.k8s.io" || specName == "pods.metrics.k8s.io" { return spec } else { log.Fatal("cant lookup "+specName+": ", err) } } spec.uuid = specName currentField := field{} scanner := bufio.NewScanner(strings.NewReader(out)) step := 0 scans := []string{} for scanner.Scan() { scans = append(scans, scanner.Text()) } //fmt.Println("--- " + specName) for u := range scans { line := scans[u] if step == 0 { if strings.HasPrefix(line, "GROUP:") { word := line[6:] spec.group = strings.Join(strings.Fields(word), " ") } else if strings.HasPrefix(line, "KIND:") { word := line[5:] spec.kind = strings.Join(strings.Fields(word), " ") step++ } step++ } else if step == 1 { if strings.HasPrefix(line, "KIND:") { word := line[5:] spec.kind = strings.Join(strings.Fields(word), " ") step++ } else { log.Fatal("cant find kind type") } } else if step == 2 { if strings.HasPrefix(line, "VERSION:") { word := line[8:] spec.version = strings.Join(strings.Fields(word), " ") step++ } else { log.Fatal("cant find kind type") } } else if step == 3 { if len(strings.TrimSpace(line)) == 0 { step++ } else { log.Fatal("cant find new line 1") } } else if step == 8 { if len(strings.TrimSpace(line)) == 0 { step = 4 } else { spec.enums += ", " + strings.TrimSpace(line) } } else if step == 5 { if len(strings.TrimSpace(line)) == 0 { step = 3 } else { if strings.TrimSpace(line) == "ENUM:" { step = 8 } else { log.Fatal("cant find new line 2 :" + line + ":") } } } else if step == 4 { if strings.HasPrefix(line, "FIELD:") { word := line[6:] spec.field = strings.Fields(word) step = 5 } else if strings.HasPrefix(line, "DESCRIPTION:") { step = 6 } else { log.Fatal("cant find field or description type") } } else if step == 6 { if !strings.HasPrefix(line, "FIELDS:") { if len(line) != 0 { spec.description = append(spec.description, strings.TrimSpace(line)) } } else { step++ } } else if step == 7 { runes := []rune(line) if strings.HasPrefix(strings.TrimSpace(line), "enum:") { currentField.enums = line[8:] } else if len(runes) > 0 && runes[0] == 32 && runes[1] == 32 && runes[2] != 32 { stringSlice := strings.Fields(line) if !(len(stringSlice) == 2 || len(stringSlice) == 3) { log.Fatal("wrong object type: " + line) } currentField.name = stringSlice[0] currentField.kind = stringSlice[1][1 : len(stringSlice[1])-1] if len(stringSlice) == 3 && stringSlice[2] == "-required-" { currentField.required = true } } else { currentField.uuid = spec.uuid currentField.description = append(currentField.description, strings.TrimSpace(line)) if len(strings.TrimSpace(line)) == 0 { if u >= len(scans)-1 { if len(strings.TrimSpace(currentField.name)) == 0 { log.Fatal("field requires a name:" + line) } if level > 0 { subSpec := getSpec(level+1, specName+"."+strings.TrimSpace(currentField.name)) currentField.fields = append(currentField.fields, subSpec.fields...) spec.specs = append(spec.specs, subSpec) } spec.fields = append(spec.fields, currentField) currentField = field{} } else { localRunes := []rune(scans[u+1]) if len(localRunes) > 0 && localRunes[0] == 32 && localRunes[1] == 32 && localRunes[2] != 32 { if len(strings.TrimSpace(currentField.name)) == 0 { log.Fatal("field requires a name:" + line) } if level > 0 { subSpec := getSpec(level+1, specName+"."+strings.TrimSpace(currentField.name)) currentField.fields = append(currentField.fields, subSpec.fields...) spec.specs = append(spec.specs, subSpec) } spec.fields = append(spec.fields, currentField) currentField = field{} } } } } } } if err := scanner.Err(); err != nil { log.Fatal("error: ", err) } if len(spec.field) != 2 && len(spec.field) != 0 { log.Fatal("type field should contain 2 or 0 elements, found " + fmt.Sprint(len(spec.field))) } if len(spec.description) > 0 { if len(strings.TrimSpace(spec.description[len(spec.description)-1])) == 0 { spec.description = spec.description[:len(spec.description)-1] } } return spec } func recurseSpecs(spec spec, arg string, dir string) { printSpec(spec, false, arg, dir) for _, subSpec := range spec.specs { if len(subSpec.specs) > 0 { recurseSpecs(subSpec, arg, dir) } } } func printSpec(spec spec, recurse bool, arg string, dir string) { var out *os.File if recurse { out = createFile(dir + "onepage_" + spec.uuid + ".html") writeFile(out, "index
") } else { out = createFile(dir + "index_" + spec.uuid + ".html") link := spec.uuid[len(arg):] if len(link) > 0 { link = link[1:] } if len(link) > 0 { if strings.LastIndex(link, ".") >= 0 { link = "." + link[:strings.LastIndex(link, ".")] } else { link = "" } } if len(arg) != len(spec.uuid) { writeFile(out, "index "+strings.ToLower(spec.kind)+link+"
") } else { writeFile(out, "index
") } } writeFile(out, "
")
	writeFile(out, "### uuid = "+spec.uuid)
	if len(spec.group) == 0 {
		writeFile(out, "group = 'default'")
	} else {
		writeFile(out, "group = '"+spec.group+"'")
	}
	writeFile(out, "kind = '"+spec.kind+"'")
	writeFile(out, "version = '"+spec.version+"'")
	writeFile(out, "field["+fmt.Sprint(len(spec.field))+"]:")
	for i := 0; i < len(spec.field); i++ {
		writeFile(out, "  "+spec.field[i])
	}
	writeFile(out, "description["+fmt.Sprint(len(spec.description))+"]:")
	for i := 0; i < len(spec.description); i++ {
		writeFile(out, "  "+spec.description[i])
	}
	writeFile(out, "fields["+fmt.Sprint(len(spec.fields))+"]:")
	printFields(1, spec.fields, len(spec.uuid), strings.ToLower(spec.kind), recurse, arg, out)

	writeFile(out, "
") closeFile(out) } func printFields(level int, fields []field, baseUuidLength int, baseName string, recurse bool, arg string, out *os.File) { fill := "" for i := 0; i < level; i++ { fill += " " } for i := range fields { linked := "" if len(fields[i].fields) > 0 { if !recurse { linked = ", link" } } if fields[i].uuid+"."+fields[i].name == "customresourcedefinitions.apiextensions.k8s.io.spec.versions.schema.openAPIV3Schema" { writeFile(out, fill+" "+fields[i].name+", "+fields[i].kind+", "+fmt.Sprint(fields[i].required)+", enums["+fields[i].enums+"]"+linked+" broken") } else { writeFile(out, fill+" "+fields[i].name+", "+fields[i].kind+", "+fmt.Sprint(fields[i].required)+", enums["+fields[i].enums+"]"+linked) } if len(fields[i].uuid) > baseUuidLength && len(fields[i].uuid[baseUuidLength+1:]) > 0 { writeFile(out, fill+" "+baseName+"."+fields[i].uuid[baseUuidLength+1:]) } else if len(fields[i].uuid) == baseUuidLength { if recurse { writeFile(out, fill+" "+baseName) } else { writeFile(out, fill+" "+baseName+fields[i].uuid[len(arg):]) } } for j := range fields[i].description { writeFile(out, fill+" "+fields[i].description[j]) } if recurse { printFields(level+1, fields[i].fields, baseUuidLength, baseName, recurse, arg, out) } } } func reverse(s []string) []string { a := make([]string, len(s)) copy(a, s) for i := len(a)/2 - 1; i >= 0; i-- { opp := len(a) - 1 - i a[i], a[opp] = a[opp], a[i] } return a } func check(e error) { if e != nil { panic(e) } } func closeFile(file *os.File) { file.Close() } func createFile(fileName string) *os.File { f, err := os.Create(fileName) check(err) //fmt.Println("created file " + fileName) return f } func writeFile(file *os.File, data string) { _, err := file.WriteString(data + "\n") check(err) }