Add a --write and --diff flag; require --write to be set before making any changes, for safety

This commit is contained in:
Pim van Pelt
2025-08-28 11:18:59 +02:00
parent 6bc0071bdb
commit d027ec9108
8 changed files with 133 additions and 26 deletions

5
go.mod
View File

@@ -3,3 +3,8 @@ module cheese
go 1.24.4 go 1.24.4
require gopkg.in/yaml.v3 v3.0.1 require gopkg.in/yaml.v3 v3.0.1
require (
github.com/hexops/gotextdiff v1.0.3 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
)

4
go.sum
View File

@@ -1,3 +1,7 @@
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@@ -8,7 +8,7 @@ import (
"strings" "strings"
) )
func generateEnv(yamlFile string) { func generateEnv(yamlFile string, wantDiff bool, allowWrite bool, useColor bool) {
config := loadConfig(yamlFile) config := loadConfig(yamlFile)
// Check that all local directories exist // Check that all local directories exist
@@ -24,7 +24,7 @@ func generateEnv(yamlFile string) {
// Create combined roots.pem file // Create combined roots.pem file
rootsPemPath := filepath.Join(logEntry.LocalDirectory, "roots.pem") rootsPemPath := filepath.Join(logEntry.LocalDirectory, "roots.pem")
err := createCombinedRootsPemWithStatus(config.Roots, logEntry.ExtraRoots, rootsPemPath) err := createCombinedRootsPemWithStatus(config.Roots, logEntry.ExtraRoots, rootsPemPath, wantDiff, allowWrite, useColor)
if err != nil { if err != nil {
log.Fatalf("Failed to create %s: %v", rootsPemPath, err) log.Fatalf("Failed to create %s: %v", rootsPemPath, err)
} }
@@ -53,14 +53,14 @@ func generateEnv(yamlFile string) {
tesseractArgs := strings.Join(args, " ") tesseractArgs := strings.Join(args, " ")
envContent := fmt.Sprintf("TESSERACT_ARGS=\"%s\"\nOTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318\n", tesseractArgs) envContent := fmt.Sprintf("TESSERACT_ARGS=\"%s\"\nOTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318\n", tesseractArgs)
err = writeFileWithStatus(envPath, []byte(envContent)) err = writeFileWithStatus(envPath, []byte(envContent), wantDiff, allowWrite, useColor)
if err != nil { if err != nil {
log.Fatalf("Failed to write %s: %v", envPath, err) log.Fatalf("Failed to write %s: %v", envPath, err)
} }
} }
} }
func createCombinedRootsPemWithStatus(rootsFile, extraRootsFile, outputPath string) error { func createCombinedRootsPemWithStatus(rootsFile, extraRootsFile, outputPath string, wantDiff bool, allowWrite bool, useColor bool) error {
// Read main roots file // Read main roots file
var combinedContent []byte var combinedContent []byte
if rootsFile != "" { if rootsFile != "" {
@@ -80,5 +80,5 @@ func createCombinedRootsPemWithStatus(rootsFile, extraRootsFile, outputPath stri
combinedContent = append(combinedContent, extraRootsData...) combinedContent = append(combinedContent, extraRootsData...)
} }
return writeFileWithStatus(outputPath, combinedContent) return writeFileWithStatus(outputPath, combinedContent, wantDiff, allowWrite, useColor)
} }

View File

@@ -110,7 +110,7 @@ type TemporalInterval struct {
EndExclusive string `json:"end_exclusive"` EndExclusive string `json:"end_exclusive"`
} }
func generateHTML(yamlFile string) { func generateHTML(yamlFile string, wantDiff bool, allowWrite bool, useColor bool) {
config := loadConfig(yamlFile) config := loadConfig(yamlFile)
// Check that all local directories exist // Check that all local directories exist
@@ -145,14 +145,14 @@ func generateHTML(yamlFile string) {
} }
// Write file with status // Write file with status
err = writeFileWithStatus(indexPath, buf.Bytes()) err = writeFileWithStatus(indexPath, buf.Bytes(), wantDiff, allowWrite, useColor)
if err != nil { if err != nil {
log.Fatalf("Failed to write HTML to %s: %v", indexPath, err) log.Fatalf("Failed to write HTML to %s: %v", indexPath, err)
} }
// Generate log.v3.json for this log // Generate log.v3.json for this log
jsonPath := filepath.Join(logEntry.LocalDirectory, "log.v3.json") jsonPath := filepath.Join(logEntry.LocalDirectory, "log.v3.json")
err = generateLogJSONWithStatus(logEntry, jsonPath) err = generateLogJSONWithStatus(logEntry, jsonPath, wantDiff, allowWrite, useColor)
if err != nil { if err != nil {
log.Fatalf("Failed to generate %s: %v", jsonPath, err) log.Fatalf("Failed to generate %s: %v", jsonPath, err)
} }
@@ -209,7 +209,7 @@ func computeKeyInfo(logEntry *Log) error {
return nil return nil
} }
func generateLogJSONWithStatus(logEntry Log, outputPath string) error { func generateLogJSONWithStatus(logEntry Log, outputPath string, wantDiff bool, allowWrite bool, useColor bool) error {
logJSON := LogV3JSON{ logJSON := LogV3JSON{
Description: fmt.Sprintf("%s.log.ct.ipng.ch", logEntry.ShortName), Description: fmt.Sprintf("%s.log.ct.ipng.ch", logEntry.ShortName),
SubmissionURL: fmt.Sprintf("%s/", logEntry.SubmissionPrefix), SubmissionURL: fmt.Sprintf("%s/", logEntry.SubmissionPrefix),
@@ -228,5 +228,5 @@ func generateLogJSONWithStatus(logEntry Log, outputPath string) error {
return fmt.Errorf("failed to marshal JSON: %v", err) return fmt.Errorf("failed to marshal JSON: %v", err)
} }
return writeFileWithStatus(outputPath, jsonData) return writeFileWithStatus(outputPath, jsonData, wantDiff, allowWrite, useColor)
} }

View File

@@ -12,7 +12,12 @@ import (
"path/filepath" "path/filepath"
) )
func generateKeys(yamlFile string) { func generateKeys(yamlFile string, wantDiff bool, allowWrite bool, useColor bool) {
if !allowWrite {
fmt.Printf("Key generation requires --write flag\n")
return
}
config := loadConfig(yamlFile) config := loadConfig(yamlFile)
// Generate keys for each log // Generate keys for each log

View File

@@ -5,8 +5,12 @@ import (
"fmt" "fmt"
"log" "log"
"os" "os"
"strings"
"time" "time"
"github.com/hexops/gotextdiff"
"github.com/hexops/gotextdiff/myers"
"github.com/hexops/gotextdiff/span"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
@@ -41,6 +45,9 @@ type Log struct {
func main() { func main() {
configFile := flag.String("c", "./tesseract-staging.yaml", "Path to the YAML configuration file") configFile := flag.String("c", "./tesseract-staging.yaml", "Path to the YAML configuration file")
wantDiff := flag.Bool("diff", false, "Show unified diff of changes")
allowWrite := flag.Bool("write", false, "Allow writing files (required for actual file modifications)")
noColor := flag.Bool("no-color", false, "Disable colored diff output")
flag.Parse() flag.Parse()
args := flag.Args() args := flag.Args()
@@ -51,15 +58,15 @@ func main() {
switch args[0] { switch args[0] {
case "gen-html": case "gen-html":
generateHTML(*configFile) generateHTML(*configFile, *wantDiff, *allowWrite, !*noColor)
case "gen-env": case "gen-env":
generateEnv(*configFile) generateEnv(*configFile, *wantDiff, *allowWrite, !*noColor)
case "gen-key": case "gen-key":
generateKeys(*configFile) generateKeys(*configFile, *wantDiff, *allowWrite, !*noColor)
case "gen-nginx": case "gen-nginx":
generateNginx(*configFile) generateNginx(*configFile, *wantDiff, *allowWrite, !*noColor)
case "gen-roots": case "gen-roots":
generateRoots(args[1:]) generateRoots(args[1:], *wantDiff, *allowWrite, !*noColor)
default: default:
fmt.Fprintf(os.Stderr, "Unknown command: %s\n", args[0]) fmt.Fprintf(os.Stderr, "Unknown command: %s\n", args[0])
showHelp() showHelp()
@@ -97,17 +104,94 @@ func loadConfig(yamlFile string) Config {
return config return config
} }
func writeFileWithStatus(filename string, content []byte) error { // ANSI color codes
const (
colorReset = "\033[0m"
colorRed = "\033[31m"
colorGreen = "\033[32m"
colorYellow = "\033[33m"
colorCyan = "\033[36m"
)
// colorizeUnifiedDiff adds ANSI color codes to unified diff output
func colorizeUnifiedDiff(diff string) string {
lines := strings.Split(diff, "\n")
var colorizedLines []string
for _, line := range lines {
switch {
case strings.HasPrefix(line, "---"):
// File deletion header in cyan
colorizedLines = append(colorizedLines, colorCyan+line+colorReset)
case strings.HasPrefix(line, "+++"):
// File addition header in cyan
colorizedLines = append(colorizedLines, colorCyan+line+colorReset)
case strings.HasPrefix(line, "@@"):
// Hunk header in yellow
colorizedLines = append(colorizedLines, colorYellow+line+colorReset)
case strings.HasPrefix(line, "-"):
// Deleted lines in red
colorizedLines = append(colorizedLines, colorRed+line+colorReset)
case strings.HasPrefix(line, "+"):
// Added lines in green
colorizedLines = append(colorizedLines, colorGreen+line+colorReset)
default:
// Context lines unchanged
colorizedLines = append(colorizedLines, line)
}
}
return strings.Join(colorizedLines, "\n")
}
func writeFileWithStatus(filename string, content []byte, wantDiff bool, allowWrite bool, useColor bool) error {
existingContent, err := os.ReadFile(filename) existingContent, err := os.ReadFile(filename)
if os.IsNotExist(err) { isNew := os.IsNotExist(err)
fmt.Printf("Creating %s\n", filename) isUnchanged := false
if isNew {
if allowWrite {
fmt.Printf("Creating %s\n", filename)
} else {
fmt.Printf("Would create %s\n", filename)
}
} else if err != nil { } else if err != nil {
return fmt.Errorf("failed to read existing file %s: %v", filename, err) return fmt.Errorf("failed to read existing file %s: %v", filename, err)
} else if string(existingContent) == string(content) { } else if string(existingContent) == string(content) {
fmt.Printf("Unchanged %s\n", filename) fmt.Printf("Unchanged %s\n", filename)
return nil isUnchanged = true
} else { } else {
fmt.Printf("Updating %s\n", filename) if allowWrite {
fmt.Printf("Updating %s\n", filename)
} else {
fmt.Printf("Would update %s\n", filename)
}
}
if wantDiff && !isUnchanged {
if isNew {
// For new files, show the entire content as added
edits := myers.ComputeEdits(span.URIFromPath(filename), "", string(content))
diff := fmt.Sprint(gotextdiff.ToUnified("/dev/null", filename, "", edits))
if useColor {
fmt.Print(colorizeUnifiedDiff(diff))
} else {
fmt.Print(diff)
}
} else {
// For existing files, show the diff
edits := myers.ComputeEdits(span.URIFromPath(filename), string(existingContent), string(content))
diff := fmt.Sprint(gotextdiff.ToUnified(filename, filename+".new", string(existingContent), edits))
if useColor {
fmt.Print(colorizeUnifiedDiff(diff))
} else {
fmt.Print(diff)
}
}
}
if isUnchanged || !allowWrite {
return nil
} }
err = os.WriteFile(filename, content, 0644) err = os.WriteFile(filename, content, 0644)
@@ -119,8 +203,17 @@ func writeFileWithStatus(filename string, content []byte) error {
func showHelp() { func showHelp() {
fmt.Printf("Usage: %s [options] <command>\n\n", os.Args[0]) fmt.Printf("Usage: %s [options] <command>\n\n", os.Args[0])
fmt.Printf("Note: Flags must come before the command name.\n\n")
fmt.Printf("Options:\n") fmt.Printf("Options:\n")
fmt.Printf(" -c <file> Path to YAML configuration file (default: ./tesseract-staging.yaml)\n\n") fmt.Printf(" -c <file> Path to YAML configuration file (default: ./tesseract-staging.yaml)\n")
fmt.Printf(" --diff Show unified diff of changes without writing files\n")
fmt.Printf(" --write Allow writing files (required for actual file modifications)\n")
fmt.Printf(" --no-color Disable colored diff output\n\n")
fmt.Printf("Examples:\n")
fmt.Printf(" %s --diff gen-html # Show colored diffs without writing\n", os.Args[0])
fmt.Printf(" %s --diff --no-color gen-html # Show plain diffs without writing\n", os.Args[0])
fmt.Printf(" %s --write gen-html # Write files\n", os.Args[0])
fmt.Printf(" %s --diff --write gen-html # Show colored diffs and write files\n\n", os.Args[0])
fmt.Printf("Commands:\n") fmt.Printf("Commands:\n")
fmt.Printf(" gen-html Generate index.html and log.v3.json files in each log's localdirectory.\n") fmt.Printf(" gen-html Generate index.html and log.v3.json files in each log's localdirectory.\n")
fmt.Printf(" Creates HTML pages with log information and CT log metadata JSON.\n") fmt.Printf(" Creates HTML pages with log information and CT log metadata JSON.\n")

View File

@@ -76,7 +76,7 @@ type NginxTemplateData struct {
ListenPort string ListenPort string
} }
func generateNginx(yamlFile string) { func generateNginx(yamlFile string, wantDiff bool, allowWrite bool, useColor bool) {
config := loadConfig(yamlFile) config := loadConfig(yamlFile)
// Extract port from first listen address // Extract port from first listen address
@@ -123,7 +123,7 @@ func generateNginx(yamlFile string) {
} }
// Write file with status // Write file with status
err = writeFileWithStatus(outputPath, buf.Bytes()) err = writeFileWithStatus(outputPath, buf.Bytes(), wantDiff, allowWrite, useColor)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "Failed to write nginx config file %s: %v\n", outputPath, err) fmt.Fprintf(os.Stderr, "Failed to write nginx config file %s: %v\n", outputPath, err)
continue continue

View File

@@ -16,7 +16,7 @@ type CTLogRootsResponse struct {
Certificates []string `json:"certificates"` Certificates []string `json:"certificates"`
} }
func generateRoots(args []string) { func generateRoots(args []string, wantDiff bool, allowWrite bool, useColor bool) {
sourceURL := "https://rennet2027h2.log.ct.ipng.ch/" sourceURL := "https://rennet2027h2.log.ct.ipng.ch/"
outputFile := "roots.pem" outputFile := "roots.pem"
@@ -107,7 +107,7 @@ func generateRoots(args []string) {
} }
// Write all certificates to file with status // Write all certificates to file with status
err = writeFileWithStatus(outputFile, pemBuffer.Bytes()) err = writeFileWithStatus(outputFile, pemBuffer.Bytes(), wantDiff, allowWrite, useColor)
if err != nil { if err != nil {
log.Fatalf("Failed to write output file %s: %v", outputFile, err) log.Fatalf("Failed to write output file %s: %v", outputFile, err)
} }