Files
coder/scripts/auditdocgen/main.go
T
Mathias Fredriksson a6a8fd94d7 build(Makefile): enable parallel make -j gen with correct dependency graph (#22612)
`make gen` could not run with `-j` because inter-target dependency edges
were missing. Multiple recipes compile `coderd/rbac` (which includes
generated files like `object_gen.go`), and without explicit ordering,
parallel runs produced syntax errors from mid-write reads.

Three main changes:

**Dependency graph fixes** declare the compile-time chain through
`coderd/rbac` so that `object_gen.go` is written before anything that
imports it is compiled. The DB generation targets use a GNU Make 4.3+
grouped target (`&:`) so Make knows `generate.sh` co-produces
`querier.go`, `unique_constraint.go`, `dbmetrics`, and `dbauthz` in a
single invocation. `SKIP_DUMP_SQL=1` avoids re-entrant `make` inside
`generate.sh` when the Makefile already guarantees `dump.sql` is fresh.

**`scripts/atomicwrite` package** replaces `os.WriteFile` in all gen
scripts with a temp-file-in-same-dir + rename pattern, preventing
interrupted runs from leaving partial files.

**`.PRECIOUS` and shell atomic writes** protect git-tracked generated
files from Make's default delete-on-error behavior. Since these files
are committed, deletion is worse than staleness -- `git restore` is the
recovery path.

CI now runs `make -j --output-sync -B gen` (~32s, down from ~85s
serial).

| Scenario                          | Before             | After    |
|-----------------------------------|--------------------|----------|
| `make gen` (serial)               | 95s                | 95s      |
| `make -j gen` (parallel)          | race error         | **22s**  |
| CI `make -j --output-sync -B gen` | forced serial ~85s | **~32s** |
2026-03-05 11:58:10 +00:00

165 lines
4.3 KiB
Go

package main
import (
"bytes"
"flag"
"log"
"os"
"sort"
"strconv"
"strings"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/enterprise/audit"
"github.com/coder/coder/v2/scripts/atomicwrite"
)
var (
auditDocFile string
dryRun bool
generatorPrefix = []byte("<!-- Code generated by 'make docs/admin/security/audit-logs.md'. DO NOT EDIT -->")
generatorSuffix = []byte("<!-- End generated by 'make docs/admin/security/audit-logs.md'. -->")
)
/*
*
AuditableResourcesMap is derived from audit.AuditableResources
and has the following structure:
{
friendlyResourceName: {
fieldName1: isTracked,
fieldName2: isTracked,
...
},
...
}
*/
type AuditableResourcesMap map[string]map[string]bool
func main() {
flag.StringVar(&auditDocFile, "audit-doc-file", "docs/admin/security/audit-logs.md", "Path to audit log doc file")
flag.BoolVar(&dryRun, "dry-run", false, "Dry run")
flag.Parse()
auditableResourcesMap := readAuditableResources()
doc, err := readAuditDoc()
if err != nil {
log.Fatal("can't read audit doc: ", err)
}
doc, err = updateAuditDoc(doc, auditableResourcesMap)
if err != nil {
log.Fatal("can't update audit doc: ", err)
}
if dryRun {
log.Println(string(doc))
return
}
err = writeAuditDoc(doc)
if err != nil {
log.Fatal("can't write updated audit doc: ", err)
}
}
// Transforms audit.AuditableResources to AuditableResourcesMap,
// which uses friendlier language.
func readAuditableResources() AuditableResourcesMap {
auditableResourcesMap := make(AuditableResourcesMap)
for resourceName, resourceFields := range audit.AuditableResources {
friendlyResourceName := strings.Split(resourceName, ".")[2]
fieldNameMap := make(map[string]bool)
for fieldName, action := range resourceFields {
fieldNameMap[fieldName] = action != audit.ActionIgnore
auditableResourcesMap[friendlyResourceName] = fieldNameMap
}
}
return auditableResourcesMap
}
func readAuditDoc() ([]byte, error) {
doc, err := os.ReadFile(auditDocFile)
if err != nil {
return nil, err
}
return doc, nil
}
// Writes a markdown table of audit log resources to a buffer
func updateAuditDoc(doc []byte, auditableResourcesMap AuditableResourcesMap) ([]byte, error) {
// We must sort the resources to ensure table ordering
sortedResourceNames := sortKeys(auditableResourcesMap)
i := bytes.Index(doc, generatorPrefix)
if i < 0 {
return nil, xerrors.New("generator prefix tag not found")
}
tableStartIndex := i + len(generatorPrefix) + 1
j := bytes.Index(doc[tableStartIndex:], generatorSuffix)
if j < 0 {
return nil, xerrors.New("generator suffix tag not found")
}
tableEndIndex := tableStartIndex + j
var buffer bytes.Buffer
_, _ = buffer.Write(doc[:tableStartIndex])
_ = buffer.WriteByte('\n')
_, _ = buffer.WriteString("|<b>Resource<b>||\n")
_, _ = buffer.WriteString("|--|-----------------|\n")
for _, resourceName := range sortedResourceNames {
readableResourceName := resourceName
// AuditableGroup is really a combination of Group and GroupMember resources
// but we use the label 'Group' in our docs to avoid confusion.
if resourceName == "AuditableGroup" {
readableResourceName = "Group"
}
// Create a string of audit actions for each resource
var auditActions []string
for _, action := range audit.AuditActionMap[readableResourceName] {
auditActions = append(auditActions, string(action))
}
auditActionsString := strings.Join(auditActions, ", ")
_, _ = buffer.WriteString("|" + readableResourceName + "<br><i>" + auditActionsString + "</i>|<table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody>" + "|")
// We must sort the field names to ensure sub-table ordering
sortedFieldNames := sortKeys(auditableResourcesMap[resourceName])
for _, fieldName := range sortedFieldNames {
isTracked := auditableResourcesMap[resourceName][fieldName]
_, _ = buffer.WriteString("<tr><td>" + fieldName + "</td><td>" + strconv.FormatBool(isTracked) + "</td></tr>")
}
_, _ = buffer.WriteString("</tbody></table>\n")
}
_, _ = buffer.WriteString("\n")
_, _ = buffer.Write(doc[tableEndIndex:])
return buffer.Bytes(), nil
}
func writeAuditDoc(doc []byte) error {
return atomicwrite.File(auditDocFile, doc)
}
func sortKeys[T any](stringMap map[string]T) []string {
var keyNames []string
for key := range stringMap {
keyNames = append(keyNames, key)
}
sort.Strings(keyNames)
return keyNames
}