github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/scripts/md-gen/controller-config/main.go (about) 1 // Copyright 2023 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package main 5 6 import ( 7 "fmt" 8 "go/ast" 9 "go/parser" 10 "go/token" 11 "os" 12 "path/filepath" 13 "reflect" 14 "sort" 15 "strings" 16 17 "github.com/juju/collections/set" 18 19 "github.com/juju/juju/controller" 20 "github.com/juju/juju/testing" 21 ) 22 23 // bidirectional mapping between key and constant name 24 // e.g. "agent-ratelimit-max" <--> "AgentRateLimitMax" 25 // These are filled by iterating through the AST of config.go 26 var ( 27 keyForConstantName = map[string]string{} 28 constantNameForKey = map[string]string{} 29 ) 30 31 // Generate Markdown documentation based on the contents of the 32 // github.com/juju/juju/controller package. 33 func main() { 34 outputDir := mustEnv("DOCS_DIR") // directory to write output to 35 jujuSrcRoot := mustEnv("JUJU_SRC_ROOT") // root of Juju source tree 36 37 data := map[string]*keyInfo{} 38 39 // Gather information from various places 40 fillFromAST(data, jujuSrcRoot) 41 fillFromConfigType(data) 42 fillFromAllowedUpdateConfigAttributes(data) 43 fillFromNewConfig(data) 44 45 _ = os.MkdirAll(outputDir, 0755) 46 render(filepath.Join(outputDir, "controller-config-keys.md"), data) 47 } 48 49 // keyInfo contains information about a config key. 50 type keyInfo struct { 51 Key string `yaml:"key"` // e.g. "agent-ratelimit-max" 52 ConstantName string `yaml:"constant-name"` // e.g. "AgentRateLimitMax" 53 Type string `yaml:"type,omitempty"` 54 Doc string `yaml:"doc,omitempty"` // from parsing comments in config.go 55 Mutable bool `yaml:"mutable"` // from AllowedUpdateConfigAttributes 56 Deprecated bool `yaml:"deprecated,omitempty"` 57 58 // Several ways of getting the default value 59 Default string `yaml:"default,omitempty"` // from instantiating NewConfig 60 Default2 string `yaml:"default2,omitempty"` // from reflection on Config type 61 } 62 63 // render turns the input data into a Markdown document 64 func render(filepath string, data map[string]*keyInfo) { 65 // Generate table of contents and main doc separately 66 var tableOfContents, mainDoc string 67 68 anchorForKey := func(key string) string { 69 return "heading--" + key 70 } 71 headingForKey := func(key string) string { 72 return fmt.Sprintf(`<a href="#%[1]s"><h2 id="%[1]s"><code>%[2]s</code></h2></a>`, 73 anchorForKey(key), key) 74 } 75 76 // Sort keys 77 var keys []string 78 for key := range data { 79 keys = append(keys, key) 80 } 81 sort.Strings(keys) 82 83 for _, key := range keys { 84 info := data[key] 85 86 tableOfContents += fmt.Sprintf("- [`%s`](#%s)\n", key, anchorForKey(key)) 87 mainDoc += headingForKey(key) + "\n" 88 if info.Deprecated { 89 mainDoc += "> This key is deprecated.\n" 90 } 91 mainDoc += "\n" 92 93 if info.Doc != "" { 94 // Ensure doc has fullstop/newlines at end 95 mainDoc += strings.TrimRight(info.Doc, ".\n") + ".\n\n" 96 } 97 if info.Type != "" { 98 mainDoc += "**Type:** " + info.Type + "\n\n" 99 } 100 if defaultVal, ok := firstNonzero(info.Default, info.Default2); ok { 101 mainDoc += "**Default value:** " + defaultVal + "\n\n" 102 } 103 104 mainDoc += "**Can be changed after bootstrap:** " 105 if info.Mutable { 106 mainDoc += "yes" 107 } else { 108 mainDoc += "no" 109 } 110 mainDoc += "\n\n\n" 111 } 112 113 err := os.WriteFile(filepath, []byte(fmt.Sprintf(` 114 > <small> [Configuration](/t/6659) > List of controller configuration keys</small> 115 > 116 > See also: [Controller](/t/5455), [How to manage configuration values for a controller](/t/1111#heading--manage-configuration-values-for-a-controller) 117 118 %s 119 120 %s 121 `[1:], tableOfContents, mainDoc)), 0644) 122 check(err) 123 } 124 125 // Gather information from the AST parsed from the Go files: 126 // ConstantName, Doc, Deprecated, Type 127 func fillFromAST(data map[string]*keyInfo, jujuSrcRoot string) { 128 controllerPkgPath := filepath.Join(jujuSrcRoot, "controller") 129 130 // Parse controller package into ASTs 131 fset := token.NewFileSet() 132 pkgs, err := parser.ParseDir(fset, controllerPkgPath, nil, parser.ParseComments) 133 check(err) 134 135 controllerConfigDotGo := pkgs["controller"].Files[filepath.Join(controllerPkgPath, "config.go")] 136 var configKeys *ast.GenDecl 137 out: 138 for _, v := range controllerConfigDotGo.Decls { 139 decl, ok := v.(*ast.GenDecl) 140 if !ok { 141 continue 142 } 143 if decl.Doc == nil { 144 continue 145 } 146 for _, comment := range decl.Doc.List { 147 if strings.Contains(comment.Text, "docs:controller-config-keys") { 148 configKeys = decl 149 break out 150 } 151 } 152 } 153 if configKeys == nil { 154 panic("unable to find const block with comment docs:controller-config-keys") 155 } 156 157 comments := map[string]string{} 158 // "keys" which should be ignored 159 ignoreKeys := map[string]struct{}{"ReadOnlyMethods": {}} 160 161 for _, spec := range configKeys.Specs { 162 valueSpec := spec.(*ast.ValueSpec) 163 key := strings.Trim(valueSpec.Values[0].(*ast.BasicLit).Value, `"`) 164 if _, ok := ignoreKeys[key]; ok { 165 continue 166 } 167 168 var comment string 169 for _, astComment := range valueSpec.Doc.List { 170 comment += strings.TrimPrefix(astComment.Text, "// ") + "\n" 171 } 172 173 constantName := valueSpec.Names[0].Name 174 keyForConstantName[constantName] = key 175 constantNameForKey[key] = constantName 176 comments[key] = comment 177 } 178 179 // Put information in data map 180 for key, comment := range comments { 181 // Replace constant names with their actual values 182 // e.g. AgentRateLimitMax --> `agent-ratelimit-max` 183 184 // Some constant names are substrings of others. To ensure we replace 185 // correctly, sort the names in descending length order first. 186 constantNames := getKeysInDescLenOrder(keyForConstantName) 187 for _, constantName := range constantNames { 188 replaceKey := keyForConstantName[constantName] 189 comment = strings.ReplaceAll(comment, constantName, fmt.Sprintf("`%s`", replaceKey)) 190 } 191 192 ensureDefined(data, key) 193 data[key].ConstantName = constantNameForKey[key] 194 data[key].Doc = comment 195 196 if strings.Contains(comment, "deprecated") || strings.Contains(comment, "Deprecated") { 197 data[key].Deprecated = true 198 } 199 } 200 201 // Pass over to configChecker AST to get types 202 fillFromConfigCheckerAST(data, 203 pkgs["controller"].Files[filepath.Join(controllerPkgPath, "configschema.go")].Decls[1].(*ast.GenDecl), 204 ) 205 } 206 207 // Get key types from parsed configChecker in configschema.go 208 func fillFromConfigCheckerAST(data map[string]*keyInfo, configChecker *ast.GenDecl) { 209 v := configChecker.Specs[0].(*ast.ValueSpec).Values[0].(*ast.CallExpr).Args 210 schemaFields := v[0].(*ast.CompositeLit) 211 212 // get key types from schemaFields 213 for _, elt := range schemaFields.Elts { 214 kvExpr := elt.(*ast.KeyValueExpr) 215 constantName := kvExpr.Key.(*ast.Ident).Name 216 key := keyForConstantName[constantName] 217 218 ensureDefined(data, key) 219 data[key].Type = typeForExpr(kvExpr.Value) 220 } 221 } 222 223 // get type from configChecker expressions 224 func typeForExpr(expr ast.Expr) string { 225 niceNames := map[string]string{ 226 "Bool": "boolean", 227 "ForceInt": "integer", 228 "List": "list", 229 "String": "string", 230 "TimeDuration": "duration", 231 } 232 niceNameFor := func(rawType string) string { 233 if nn, ok := niceNames[rawType]; ok { 234 return nn 235 } 236 return rawType 237 } 238 239 callExpr := expr.(*ast.CallExpr) 240 rawType := callExpr.Fun.(*ast.SelectorExpr).Sel.Name 241 dataType := niceNameFor(rawType) 242 243 if len(callExpr.Args) > 0 { 244 // add parameter types 245 dataType += "[" 246 for i, arg := range callExpr.Args { 247 if i > 0 { 248 dataType += ", " 249 } 250 dataType += typeForExpr(arg) 251 } 252 dataType += "]" 253 } 254 255 return dataType 256 } 257 258 // Check whether key is mutable in AllowedUpdateConfigAttributes slice 259 func fillFromAllowedUpdateConfigAttributes(data map[string]*keyInfo) { 260 for key := range controller.AllowedUpdateConfigAttributes { 261 ensureDefined(data, key) 262 data[key].Mutable = true 263 } 264 } 265 266 // keys for which a default value doesn't make sense 267 var skipDefault = set.NewStrings( 268 controller.AuditLogExcludeMethods, // "[ReadOnlyMethods]" - not useful 269 controller.CACertKey, 270 controller.ControllerUUIDKey, 271 ) 272 273 // Get default values using reflection on controller.Config type 274 func fillFromNewConfig(data map[string]*keyInfo) { 275 config, err := controller.NewConfig(testing.ControllerTag.Id(), testing.CACert, nil) 276 check(err) 277 for key, defaultVal := range config { 278 if skipDefault.Contains(key) { 279 continue 280 } 281 282 ensureDefined(data, key) 283 data[key].Default = fmt.Sprint(defaultVal) 284 } 285 } 286 287 // Get default values using reflection on controller.Config type 288 // Used as a fallback where fillFromNewConfig can't produce a value 289 func fillFromConfigType(data map[string]*keyInfo) { 290 // Don't get defaults from these methods - generally bogus values 291 skipMethods := set.NewStrings( 292 "CAASImageRepo", 293 "CAASOperatorImagePath", 294 "ControllerAPIPort", 295 "Features", 296 "IdentityPublicKey", 297 "Validate", // not a config key 298 ) 299 300 constantNameForMethod := func(methodName string) string { 301 name := strings.TrimSuffix(methodName, "MB") 302 303 rename := map[string]string{ 304 "NUMACtlPreference": "SetNUMAControlPolicyKey", 305 } 306 if rn, ok := rename[name]; ok { 307 name = rn 308 } 309 310 return name 311 } 312 313 config, err := controller.NewConfig(testing.ControllerTag.Id(), testing.CACert, nil) 314 check(err) 315 t := reflect.TypeOf(config) 316 v := reflect.ValueOf(config) 317 318 for i := 0; i < t.NumMethod(); i++ { 319 method := t.Method(i) 320 methodValue := v.Method(i) 321 322 if skipMethods.Contains(method.Name) { 323 continue 324 } 325 if method.Type.NumIn() == 1 { 326 defaultVal := methodValue.Call([]reflect.Value{})[0] 327 328 constantName := constantNameForMethod(method.Name) 329 key, ok := keyForConstantName[constantName] 330 if !ok { 331 // Try adding "Key" suffix 332 key, ok = keyForConstantName[constantName+"Key"] 333 if !ok { 334 panic(method.Name) 335 } 336 } 337 if skipDefault.Contains(key) { 338 continue 339 } 340 341 ensureDefined(data, key) 342 data[key].Default2 = fmt.Sprint(defaultVal) 343 } 344 } 345 } 346 347 // UTILITY FUNCTIONS 348 349 // Returns the value of the given environment variable, panicking if the var 350 // is not set. 351 func mustEnv(key string) string { 352 val, ok := os.LookupEnv(key) 353 if !ok { 354 panic(fmt.Sprintf("env var %q not set", key)) 355 } 356 return val 357 } 358 359 // Return the first value that is defined / not-zero, and "true" 360 // if such a value is found. 361 func firstNonzero(vals ...string) (string, bool) { 362 for _, val := range vals { 363 if val != "" { 364 return val, true 365 } 366 } 367 return "", false 368 } 369 370 // Ensure that the data map has an entry for key. 371 func ensureDefined(data map[string]*keyInfo, key string) { 372 if data[key] == nil { 373 data[key] = &keyInfo{ 374 Key: key, 375 } 376 } 377 } 378 379 // check panics if the provided error is nil. 380 func check(err error) { 381 if err != nil { 382 panic(err) 383 } 384 } 385 386 // return keys of the given map in descending length order 387 func getKeysInDescLenOrder[T any](m map[string]T) (keys []string) { 388 for k := range m { 389 keys = append(keys, k) 390 } 391 sort.Slice(keys, func(i, j int) bool { 392 return len(keys[i]) > len(keys[j]) 393 }) 394 return 395 }