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  }