diff --git a/docs/src/_parts/bootstrap_config.md b/docs/src/_parts/bootstrap_config.md new file mode 100644 index 0000000000..5754a285ff --- /dev/null +++ b/docs/src/_parts/bootstrap_config.md @@ -0,0 +1,291 @@ +### cluster-config.network.enabled +**Type:** `bool`
+ + +### cluster-config.dns.enabled +**Type:** `bool`
+ + +### cluster-config.dns.cluster-domain +**Type:** `string`
+ + +### cluster-config.dns.service-ip +**Type:** `string`
+ + +### cluster-config.dns.upstream-nameservers +**Type:** `[]string`
+ + +### cluster-config.ingress.enabled +**Type:** `bool`
+ + +### cluster-config.ingress.default-tls-secret +**Type:** `string`
+ + +### cluster-config.ingress.enable-proxy-protocol +**Type:** `bool`
+ + +### cluster-config.load-balancer.enabled +**Type:** `bool`
+ + +### cluster-config.load-balancer.cidrs +**Type:** `[]string`
+ + +### cluster-config.load-balancer.l2-mode +**Type:** `bool`
+ + +### cluster-config.load-balancer.l2-interfaces +**Type:** `[]string`
+ + +### cluster-config.load-balancer.bgp-mode +**Type:** `bool`
+ + +### cluster-config.load-balancer.bgp-local-asn +**Type:** `int`
+ + +### cluster-config.load-balancer.bgp-peer-address +**Type:** `string`
+ + +### cluster-config.load-balancer.bgp-peer-asn +**Type:** `int`
+ + +### cluster-config.load-balancer.bgp-peer-port +**Type:** `int`
+ + +### cluster-config.local-storage.enabled +**Type:** `bool`
+ + +### cluster-config.local-storage.local-path +**Type:** `string`
+ + +### cluster-config.local-storage.reclaim-policy +**Type:** `string`
+ + +### cluster-config.local-storage.default +**Type:** `bool`
+ + +### cluster-config.gateway.enabled +**Type:** `bool`
+ + +### cluster-config.metrics-server.enabled +**Type:** `bool`
+ + +### cluster-config.cloud-provider +**Type:** `string`
+ + +### cluster-config.annotations +**Type:** `map[string]string`
+ + +### control-plane-taints +**Type:** `[]string`
+ +Seed configuration for the control plane (flat on purpose). Empty values are ignored + +### pod-cidr +**Type:** `string`
+ + +### service-cidr +**Type:** `string`
+ + +### disable-rbac +**Type:** `bool`
+ + +### secure-port +**Type:** `int`
+ + +### k8s-dqlite-port +**Type:** `int`
+ + +### datastore-type +**Type:** `string`
+ + +### datastore-servers +**Type:** `[]string`
+ + +### datastore-ca-crt +**Type:** `string`
+ + +### datastore-client-crt +**Type:** `string`
+ + +### datastore-client-key +**Type:** `string`
+ + +### extra-sans +**Type:** `[]string`
+ +Seed configuration for certificates + +### ca-crt +**Type:** `string`
+ +Seed configuration for external certificates (cluster-wide) + +### ca-key +**Type:** `string`
+ + +### client-ca-crt +**Type:** `string`
+ + +### client-ca-key +**Type:** `string`
+ + +### front-proxy-ca-crt +**Type:** `string`
+ + +### front-proxy-ca-key +**Type:** `string`
+ + +### front-proxy-client-crt +**Type:** `string`
+ + +### front-proxy-client-key +**Type:** `string`
+ + +### apiserver-kubelet-client-crt +**Type:** `string`
+ + +### apiserver-kubelet-client-key +**Type:** `string`
+ + +### admin-client-crt +**Type:** `string`
+ + +### admin-client-key +**Type:** `string`
+ + +### kube-proxy-client-crt +**Type:** `string`
+ + +### kube-proxy-client-key +**Type:** `string`
+ + +### kube-scheduler-client-crt +**Type:** `string`
+ + +### kube-scheduler-client-key +**Type:** `string`
+ + +### kube-controller-manager-client-crt +**Type:** `string`
+ + +### kube-controller-manager-client-key +**Type:** `string`
+ + +### service-account-key +**Type:** `string`
+ + +### apiserver-crt +**Type:** `string`
+ +Seed configuration for external certificates (node-specific) + +### apiserver-key +**Type:** `string`
+ + +### kubelet-crt +**Type:** `string`
+ + +### kubelet-key +**Type:** `string`
+ + +### kubelet-client-crt +**Type:** `string`
+ + +### kubelet-client-key +**Type:** `string`
+ + +### extra-node-config-files +**Type:** `map[string]string`
+ +ExtraNodeConfigFiles will be written to /var/snap/k8s/common/args/conf.d + +### extra-node-kube-apiserver-args +**Type:** `map[string]string`
+ +Extra args to add to individual services (set any arg to null to delete) + +### extra-node-kube-controller-manager-args +**Type:** `map[string]string`
+ + +### extra-node-kube-scheduler-args +**Type:** `map[string]string`
+ + +### extra-node-kube-proxy-args +**Type:** `map[string]string`
+ + +### extra-node-kubelet-args +**Type:** `map[string]string`
+ + +### extra-node-containerd-args +**Type:** `map[string]string`
+ + +### extra-node-k8s-dqlite-args +**Type:** `map[string]string`
+ + +### extra-node-containerd-config +**Type:** `apiv1.MapStringAny`
+ +Extra configuration for the containerd config.toml + diff --git a/docs/src/snap/reference/bootstrap-config-reference.md b/docs/src/snap/reference/bootstrap-config-reference.md index 047622c5d9..4cc53764f0 100644 --- a/docs/src/snap/reference/bootstrap-config-reference.md +++ b/docs/src/snap/reference/bootstrap-config-reference.md @@ -4,517 +4,9 @@ A YAML file can be supplied to the `k8s bootstrap` command to configure and customise the cluster. This reference section provides the format of this file by listing all available options and their details. See below for an example. -## Format Specification - -### cluster-config.network - -**Type:** `object`
-**Required:** `No` - -Configuration options for the network feature - -#### cluster-config.network.enabled - -**Type:** `bool`
-**Required:** `No`
- -Determines if the feature should be enabled. -If omitted defaults to `true` - -### cluster-config.dns - -**Type:** `object`
-**Required:** `No` - -Configuration options for the dns feature - -#### cluster-config.dns.enabled - -**Type:** `bool`
-**Required:** `No`
- -Determines if the feature should be enabled. -If omitted defaults to `true` - -#### cluster-config.dns.cluster-domain - -**Type:** `string`
-**Required:** `No`
- -Sets the local domain of the cluster. -If omitted defaults to `cluster.local` - -#### cluster-config.dns.service-ip - -**Type:** `string`
-**Required:** `No`
- -Sets the IP address of the dns service. If omitted defaults to the IP address -of the Kubernetes service created by the feature. - -Can be used to point to an external dns server when feature is disabled. - - -#### cluster-config.dns.upstream-nameservers - -**Type:** `list[string]`
-**Required:** `No`
- -Sets the upstream nameservers used to forward queries for out-of-cluster -endpoints. -If omitted defaults to `/etc/resolv.conf` and uses the nameservers of the node. - - -### cluster-config.ingress - -**Type:** `object`
-**Required:** `No` - -Configuration options for the ingress feature - -#### cluster-config.ingress.enabled - -**Type:** `bool`
-**Required:** `No`
- -Determines if the feature should be enabled. -If omitted defaults to `false` - -#### cluster-config.ingress.default-tls-secret - -**Type:** `string`
-**Required:** `No`
- -Sets the name of the secret to be used for providing default encryption to -ingresses. - -Ingresses can specify another TLS secret in their resource definitions, -in which case the default secret won't be used. - -#### cluster-config.ingress.enable-proxy-protocol - -**Type:** `bool`
-**Required:** `No`
- -Determines if the proxy protocol should be enabled for ingresses. -If omitted defaults to `false` - - -### cluster-config.load-balancer - -**Type:** `object`
-**Required:** `No` - -Configuration options for the load-balancer feature - -#### cluster-config.load-balancer.enabled - -**Type:** `bool`
-**Required:** `No`
- -Determines if the feature should be enabled. -If omitted defaults to `false` - -#### cluster-config.load-balancer.cidrs - -**Type:** `list[string]`
-**Required:** `No`
- -Sets the CIDRs used for assigning IP addresses to Kubernetes services with type -`LoadBalancer`. - -#### cluster-config.load-balancer.l2-mode - -**Type:** `bool`
-**Required:** `No`
- -Determines if L2 mode should be enabled. -If omitted defaults to `false` - -#### cluster-config.load-balancer.l2-interfaces - -**Type:** `list[string]`
-**Required:** `No`
- -Sets the interfaces to be used for announcing IP addresses through ARP. -If omitted all interfaces will be used. - -#### cluster-config.load-balancer.bgp-mode - -**Type:** `bool`
-**Required:** `No`
- -Determines if BGP mode should be enabled. -If omitted defaults to `false` - -#### cluster-config.load-balancer.bgp-local-asn - -**Type:** `int`
-**Required:** `Yes if bgp-mode is true`
- -Sets the ASN to be used for the local virtual BGP router. - -#### cluster-config.load-balancer.bgp-peer-address - -**Type:** `string`
-**Required:** `Yes if bgp-mode is true`
- -Sets the IP address of the BGP peer. - -#### cluster-config.load-balancer.bgp-peer-asn - -**Type:** `int`
-**Required:** `Yes if bgp-mode is true`
- -Sets the ASN of the BGP peer. - -#### cluster-config.load-balancer.bgp-peer-port - -**Type:** `int`
-**Required:** `Yes if bgp-mode is true`
- -Sets the port of the BGP peer. - - -### cluster-config.local-storage - -**Type:** `object`
-**Required:** `No` - -Configuration options for the local-storage feature - -#### cluster-config.local-storage.enabled - -**Type:** `bool`
-**Required:** `No`
- -Determines if the feature should be enabled. -If omitted defaults to `false` - -#### cluster-config.local-storage.local-path - -**Type:** `string`
-**Required:** `No`
- -Sets the path to be used for storing volume data. -If omitted defaults to `/var/snap/k8s/common/rawfile-storage` - -#### cluster-config.local-storage.reclaim-policy - -**Type:** `string`
-**Required:** `No`
-**Possible Values:** `Retain | Recycle | Delete` - -Sets the reclaim policy of the storage class. -If omitted defaults to `Delete` - -#### cluster-config.local-storage.default - -**Type:** `bool`
-**Required:** `No`
- -Determines if the storage class should be set as default. -If omitted defaults to `true` - - -### cluster-config.gateway - -**Type:** `object`
-**Required:** `No` - -Configuration options for the gateway feature - -#### cluster-config.gateway.enabled - -**Type:** `bool`
-**Required:** `No`
- -Determines if the feature should be enabled. -If omitted defaults to `true` - -### cluster-config.cloud-provider - -**Type:** `string`
-**Required:** `No`
-**Possible Values:** `external` - -Sets the cloud provider to be used by the cluster. - -When this is set as `external`, node will wait for an external cloud provider to -do cloud specific setup and finish node initialization. - -### control-plane-taints - -**Type:** `list[string]`
-**Required:** `No` - -List of taints to be applied to control plane nodes. - -### pod-cidr - -**Type:** `string`
-**Required:** `No` - -The CIDR to be used for assigning pod addresses. -If omitted defaults to `10.1.0.0/16` - -### service-cidr - -**Type:** `string`
-**Required:** `No` - -The CIDR to be used for assigning service addresses. -If omitted defaults to `10.152.183.0/24` - -### disable-rbac - -**Type:** `bool`
-**Required:** `No` - -Determines if RBAC should be disabled. -If omitted defaults to `false` - -### secure-port - -**Type:** `int`
-**Required:** `No` - -The port number for kube-apiserver to use. -If omitted defaults to `6443` - -### k8s-dqlite-port - -**Type:** `int`
-**Required:** `No` - -The port number for k8s-dqlite to use. -If omitted defaults to `9000` - -### datastore-type - -**Type:** `string`
-**Required:** `No`
-**Possible Values:** `k8s-dqlite | external` - -The type of datastore to be used. -If omitted defaults to `k8s-dqlite` - -Can be used to point to an external datastore like etcd. - -### datastore-servers - -**Type:** `list[string]`
-**Required:** `No`
- -The server addresses to be used when `datastore-type` is set to `external`. - -### datastore-ca-crt - -**Type:** `string`
-**Required:** `No`
- -The CA certificate to be used when communicating with the external datastore. - -### datastore-client-crt - -**Type:** `string`
-**Required:** `No`
- -The client certificate to be used when communicating with the external -datastore. - -### datastore-client-key - -**Type:** `string`
-**Required:** `No`
- -The client key to be used when communicating with the external datastore. - -### extra-sans - -**Type:** `list[string]`
-**Required:** `No`
- -List of extra SANs to be added to certificates. - -### ca-crt - -**Type:** `string`
-**Required:** `No`
- -The CA certificate to be used for Kubernetes services. -If omitted defaults to an auto generated certificate. - -### ca-key - -**Type:** `string`
-**Required:** `No`
- -The CA key to be used for Kubernetes services. -If omitted defaults to an auto generated key. - -### front-proxy-ca-crt - -**Type:** `string`
-**Required:** `No`
- -The CA certificate to be used for the front proxy. -If omitted defaults to an auto generated certificate. - -### front-proxy-ca-key - -**Type:** `string`
-**Required:** `No`
- -The CA key to be used for the front proxy. -If omitted defaults to an auto generated key. - -### front-proxy-client-crt - -**Type:** `string`
-**Required:** `No`
- -The client certificate to be used for the front proxy. -If omitted defaults to an auto generated certificate. - -### front-proxy-client-key - -**Type:** `string`
-**Required:** `No`
- -The client key to be used for the front proxy. -If omitted defaults to an auto generated key. - - -### apiserver-kubelet-client-crt - -**Type:** `string`
-**Required:** `No`
- -The client certificate to be used by kubelet for communicating with the -kube-apiserver. -If omitted defaults to an auto generated certificate. - -### apiserver-kubelet-client-key - -**Type:** `string`
-**Required:** `No`
- -The client key to be used by kubelet for communicating with the kube-apiserver. -If omitted defaults to an auto generated key. - -### service-account-key - -**Type:** `string`
-**Required:** `No`
- -The key to be used by the default service account. -If omitted defaults to an auto generated key. - -### apiserver-crt - -**Type:** `string`
-**Required:** `No`
- -The certificate to be used for the kube-apiserver. -If omitted defaults to an auto generated certificate. - -### apiserver-key - -**Type:** `string`
-**Required:** `No`
- -The key to be used for the kube-apiserver. -If omitted defaults to an auto generated key. - -### kubelet-crt - -**Type:** `string`
-**Required:** `No`
- -The certificate to be used for the kubelet. -If omitted defaults to an auto generated certificate. - -### kubelet-key - -**Type:** `string`
-**Required:** `No`
- -The key to be used for the kubelet. -If omitted defaults to an auto generated key. - -### extra-node-config-files - -**Type:** `map[string]string`
-**Required:** `No`
- -Additional files that are uploaded `/var/snap/k8s/common/args/conf.d/` -to a node on bootstrap. These files can them be references by Kubernetes -service arguments. -The format is `map[]`. - -### extra-node-kube-apiserver-args - -**Type:** `map[string]string`
-**Required:** `No`
- -Additional arguments that are passed to the `kube-apiserver` only for that -specific node. Overwrites default configuration. A parameter that is explicitly -set to `null` is deleted. The format is `map[<--flag-name>]`. - -### extra-node-kube-controller-manager-args - -**Type:** `map[string]string`
-**Required:** `No`
- -Additional arguments that are passed to the `kube-controller-manager` only for -that specific node. Overwrites default configuration. A parameter that is -explicitly set to `null` is deleted. The format is `map[<--flag-name>]`. - -### extra-node-kube-scheduler-args - -**Type:** `map[string]string`
-**Required:** `No`
- -Additional arguments that are passed to the `kube-scheduler` only for that -specific node. Overwrites default configuration. A parameter that is explicitly -set to `null` is deleted. The format is `map[<--flag-name>]`. - -### extra-node-kube-proxy-args - -**Type:** `map[string]string`
-**Required:** `No`
- -Additional arguments that are passed to the `kube-proxy` only for that -specific node. Overwrites default configuration. A parameter that is explicitly -set to `null` is deleted. The format is `map[<--flag-name>]`. - -### extra-node-kubelet-args - -**Type:** `map[string]string`
-**Required:** `No`
- -Additional arguments that are passed to the `kubelet` only for that -specific node. Overwrites default configuration. A parameter that is explicitly -set to `null` is deleted. The format is `map[<--flag-name>]`. - -### extra-node-containerd-args - -**Type:** `map[string]string`
-**Required:** `No`
- -Additional arguments that are passed to `containerd` only for that -specific node. Overwrites default configuration. A parameter that is explicitly -set to `null` is deleted. The format is `map[<--flag-name>]`. - -### extra-node-k8s-dqlite-args - -**Type:** `map[string]string`
-**Required:** `No`
+```{include} ../../_parts/bootstrap_config.md +``` -Additional arguments that are passed to `k8s-dqlite` only for that -specific node. Overwrites default configuration. A parameter that is explicitly -set to `null` is deleted. The format is `map[<--flag-name>]`. ## Example diff --git a/src/k8s/Makefile b/src/k8s/Makefile index 4e7d331543..389a050d5c 100644 --- a/src/k8s/Makefile +++ b/src/k8s/Makefile @@ -14,7 +14,7 @@ go.unit: $(DQLITE_BUILD_SCRIPTS_DIR)/static-go-test.sh -v ./pkg/... ./cmd/... -coverprofile=coverage.txt --cover go.doc: bin/static/k8s - bin/static/k8s generate-docs --output-dir ../../docs/src/_parts/commands/ + bin/static/k8s generate-docs --output-dir ../../docs/src/_parts/ ## Static Builds static: bin/static/k8s bin/static/k8sd bin/static/k8s-apiserver-proxy diff --git a/src/k8s/cmd/k8s/k8s_generate_docs.go b/src/k8s/cmd/k8s/k8s_generate_docs.go index 10a8ea7158..53a95409b4 100644 --- a/src/k8s/cmd/k8s/k8s_generate_docs.go +++ b/src/k8s/cmd/k8s/k8s_generate_docs.go @@ -1,7 +1,11 @@ package k8s import ( + "os" + + apiv1 "github.com/canonical/k8s-snap-api/api/v1" cmdutil "github.com/canonical/k8s/cmd/util" + "github.com/canonical/k8s/pkg/docgen" "github.com/spf13/cobra" "github.com/spf13/cobra/doc" ) @@ -15,11 +19,29 @@ func newGenerateDocsCmd(env cmdutil.ExecutionEnvironment) *cobra.Command { Hidden: true, Short: "Generate markdown documentation", Run: func(cmd *cobra.Command, args []string) { - if err := doc.GenMarkdownTree(cmd.Parent(), opts.outputDir); err != nil { + if err := doc.GenMarkdownTree(cmd.Parent(), opts.outputDir + "/commands"); err != nil { cmd.PrintErrf("Error: Failed to generate markdown documentation for k8s command.\n\nThe error was: %v\n", err) env.Exit(1) return } + + bootstrap_doc, err := docgen.MarkdownFromJsonStruct(apiv1.BootstrapConfig{}) + if err != nil { + cmd.PrintErrf("Error: Failed to generate markdown documentation for bootstrap configuration\n\n") + cmd.PrintErrf("Error: %v", err) + env.Exit(1) + return + } + + bootstrap_doc_path := opts.outputDir + "/bootstrap_config.md" + err = os.WriteFile(bootstrap_doc_path, []byte(bootstrap_doc), 0644) + if err != nil { + cmd.PrintErrf("Error: Failed to write markdown documentation for bootstrap configuration\n\n") + cmd.PrintErrf("Error: %v", ) + env.Exit(1) + return + } + cmd.Printf("Generated documentation in %s\n", opts.outputDir) }, } diff --git a/src/k8s/go.mod b/src/k8s/go.mod index ca4a888d66..45ee278b8d 100644 --- a/src/k8s/go.mod +++ b/src/k8s/go.mod @@ -14,6 +14,7 @@ require ( github.com/onsi/gomega v1.32.0 github.com/pelletier/go-toml v1.9.5 github.com/spf13/cobra v1.8.1 + golang.org/x/mod v0.20.0 golang.org/x/net v0.28.0 golang.org/x/sync v0.8.0 golang.org/x/sys v0.24.0 diff --git a/src/k8s/pkg/docgen/godoc.go b/src/k8s/pkg/docgen/godoc.go new file mode 100755 index 0000000000..a875c32512 --- /dev/null +++ b/src/k8s/pkg/docgen/godoc.go @@ -0,0 +1,117 @@ +package docgen + +import ( + "fmt" + "go/ast" + "go/doc" + "go/parser" + "go/token" + "reflect" +) + +var packageDocCache = make(map[string]*doc.Package) + + +func findTypeSpec(decl *ast.GenDecl, symbol string) *ast.TypeSpec { + for _, spec := range decl.Specs { + typeSpec := spec.(*ast.TypeSpec) + if symbol == typeSpec.Name.Name { + return typeSpec + } + } + return nil +} + +func getStructTypeFromDoc(packageDoc *doc.Package, structName string) *ast.StructType { + for _, docType := range packageDoc.Types { + if structName != docType.Name { + continue + } + typeSpec := findTypeSpec(docType.Decl, docType.Name) + structType, ok := typeSpec.Type.(*ast.StructType) + if !ok { + // Not a structure. + continue + } + return structType; + } + return nil +} + +func parsePackageDir(packageDir string) (*ast.Package, error) { + fset := token.NewFileSet() + packages, err := parser.ParseDir(fset, packageDir, nil, parser.ParseComments) + if err != nil { + return nil, fmt.Errorf("couldn't parse go package: %s", packageDir) + } + + if len(packages) == 0 { + return nil, fmt.Errorf("no go package found: %s", packageDir) + } + if len(packages) > 1 { + return nil, fmt.Errorf("multiple go package found: %s", packageDir) + } + + // We have a map containing a single entry and we need to return it. + for _, pkg := range packages { + return pkg, nil + } + + // shouldn't really get here. + return nil, fmt.Errorf("failed to parse go package") +} + + +func getAstStructField(structType *ast.StructType, fieldName string) *ast.Field { + for _, field := range structType.Fields.List { + for _, fieldIdent := range field.Names { + if fieldIdent.Name == fieldName { + return field + } + } + } + return nil +} + +func getPackageDoc(packagePath string) (*doc.Package, error) { + packageDoc, found := packageDocCache[packagePath] + if found { + return packageDoc, nil + } + + packageDir, err := getGoPackageDir(packagePath) + if err != nil { + return nil, fmt.Errorf("couldn't retrieve package dir, error: %v", err) + } + + pkg, err := parsePackageDir(packageDir) + if err != nil { + return nil, err + } + + packageDoc = doc.New(pkg, packageDir, doc.AllDecls | doc.PreserveAST) + packageDocCache[packagePath] = packageDoc + + return packageDoc, nil +} + +func getFieldDocstring(i any, field reflect.StructField) (string, error) { + inType := reflect.TypeOf(i) + + packageDoc, err := getPackageDoc(inType.PkgPath()) + if err != nil { + return "", err + } + + structType := getStructTypeFromDoc(packageDoc, inType.Name()) + if structType == nil { + return "", fmt.Errorf("could not find %s structure definition", inType.Name) + } + + astField := getAstStructField(structType, field.Name) + if astField == nil { + return "", fmt.Errorf("could not find %s.%s field definition", inType.Name, field.Name) + } + + return astField.Doc.Text(), nil +} diff --git a/src/k8s/pkg/docgen/gomod.go b/src/k8s/pkg/docgen/gomod.go new file mode 100755 index 0000000000..8b621ec692 --- /dev/null +++ b/src/k8s/pkg/docgen/gomod.go @@ -0,0 +1,125 @@ +package docgen + +import ( + "fmt" + "os" + "path" + "path/filepath" + "strings" + "golang.org/x/mod/modfile" + "golang.org/x/mod/module" +) + +func getGoDepModulePath(name string, version string) (string, error) { + cachePath := os.Getenv("GOMODCACHE") + if cachePath == "" { + goPath := os.Getenv("GOPATH") + if goPath == "" { + goPath = path.Join(os.Getenv("HOME"), "/go") + } + cachePath = path.Join(goPath, "pkg", "mod") + } + + escapedPath, err := module.EscapePath(name) + if err != nil { + return "", fmt.Errorf( + "couldn't escape module path: %s %v", name, err) + } + + escapedVersion, err := module.EscapeVersion(version) + if err != nil { + return "", fmt.Errorf( + "couldn't escape module version: %s %v", version, err) + } + + path := path.Join(cachePath, escapedPath+"@"+escapedVersion) + + // Validate the path. + if _, err := os.Stat(path); err != nil { + return "", fmt.Errorf( + "Go module path not accessible: %s %s %s. Error: %v.", + name, version, path, err) + } + + return path, nil +} + +func getDependencyVersionFromGoMod(goModPath string, packageName string, directOnly bool) (string, string, error) { + goModContents, err := os.ReadFile(goModPath) + if err != nil { + return "", "", fmt.Errorf("could not read go.mod file %s. Error: ", goModPath, err) + } + goModFile, err := modfile.ParseLax(goModPath, goModContents, nil) + if err != nil { + return "", "", fmt.Errorf("could not parse go.mod file %s. Error: ", goModPath, err) + } + + for _, dep := range goModFile.Require { + if directOnly && dep.Indirect { + continue + } + if strings.HasPrefix(packageName, dep.Mod.Path) { + return dep.Mod.Path, dep.Mod.Version, nil + } + } + + return "", "", fmt.Errorf("could not find dependency %s in %s", packageName, goModPath) +} + +// getProjectDir retrieves the full path of k8s-snap/src/k8s. +// For simplicity, we assume that the executable is placed in +// k8s-snap/src/k8s/bin/(static|dynamic). +// This will mostly be used to generate the project documentation using +// "make go.doc". +func getProjectDir() (string, error) { + exec, err := os.Executable() + if err != nil { + return "", fmt.Errorf("couldn't retrieve executable path, error: %v", err) + } + + projDir := path.Join(filepath.Dir(exec), "..", "..") + return filepath.Abs(projDir) +} + +func getGoModPath() (string, error) { + projDir, err := getProjectDir() + if err != nil { + return "", err + } + + return path.Join(projDir, "go.mod"), nil +} + +func getGoPackageDir(packageName string) (string, error) { + if packageName == "" { + return "", fmt.Errorf("could not retrieve package dir, no package name specified.") + } + + if strings.HasPrefix(packageName, "github.com/canonical/k8s/") { + projDir, err := getProjectDir() + if err != nil { + return "", err + } + + return strings.Replace(packageName, "github.com/canonical/k8s", projDir, 1), nil + } + + // Dependency, need to retrieve its version from go.mod + goModPath, err := getGoModPath() + if err != nil { + return "", err + } + + basePackageName, version, err := getDependencyVersionFromGoMod(goModPath, packageName, false) + if err != nil { + return "", err + } + + basePath, err := getGoDepModulePath(basePackageName, version) + if err != nil { + return "", err + } + + subPath := strings.TrimPrefix(packageName, basePackageName) + return path.Join(basePath, subPath), nil +} diff --git a/src/k8s/pkg/docgen/json_struct.go b/src/k8s/pkg/docgen/json_struct.go new file mode 100755 index 0000000000..4352dc6baf --- /dev/null +++ b/src/k8s/pkg/docgen/json_struct.go @@ -0,0 +1,109 @@ +package docgen + +import ( + "fmt" + "os" + "reflect" + "strings" +) + +type JsonTag struct { + Name string + Options []string +} + +type Field struct { + Name string + TypeName string + JsonTag JsonTag + FullJsonPath string + Docstring string +} + +// Generate Markdown documentation for a JSON or YAML based on +// the Go structure definition, parsing field annotations. +func MarkdownFromJsonStruct(i any) (string, error) { + fields, err := ParseStruct(i) + if err != nil { + return "", err + } + + entryTemplate := `### %s +**Type:** `+"`%s`" +`
+ +%s +` + + var out strings.Builder + for _, field := range fields { + outFieldType := strings.Replace(field.TypeName, "*", "", -1) + entry := fmt.Sprintf(entryTemplate, field.FullJsonPath, outFieldType, field.Docstring) + out.WriteString(entry) + } + + return out.String(), nil +} + +func getJsonTag(field reflect.StructField) JsonTag { + jsonTag := JsonTag{} + + jsonTagStr := field.Tag.Get("json") + if jsonTagStr == "" { + // Use yaml tags as fallback, which have the same format. + jsonTagStr = field.Tag.Get("yaml") + } + if jsonTagStr != "" { + jsonTagSlice := strings.Split(jsonTagStr, ",") + if len(jsonTagSlice) > 0 { + jsonTag.Name = jsonTagSlice[0] + } + if len(jsonTagSlice) > 1 { + jsonTag.Options = jsonTagSlice[1:] + } + } + + return jsonTag +} + +func ParseStruct(i any) ([]Field, error) { + inType := reflect.TypeOf(i) + + if inType.Kind() != reflect.Struct { + return nil, fmt.Errorf("structure parsing failed, not a structure: %s", inType.Name) + } + + outFields := []Field{} + fields := reflect.VisibleFields(inType) + for _, field := range fields { + jsonTag := getJsonTag(field) + docstring, err := getFieldDocstring(i, field) + if err != nil { + fmt.Fprintf(os.Stderr, "WARNING: could not retrieve field docstring: %s.%s, error: %v", + inType.Name, field.Name, err) + } + + if field.Type.Kind() == reflect.Struct { + fieldIface := reflect.ValueOf(i).FieldByName(field.Name).Interface() + nestedFields, err := ParseStruct(fieldIface) + if err != nil { + return nil, fmt.Errorf("couldn't parse %s.%s, error: %v", inType, field.Name, err) + } + for _, nestedField := range nestedFields { + // Update the json paths of the nested fields based on the field name. + nestedField.FullJsonPath = jsonTag.Name + "." + nestedField.FullJsonPath + outFields = append(outFields, nestedField) + } + } else { + outField := Field{ + Name: field.Name, + TypeName: field.Type.String(), + JsonTag: jsonTag, + FullJsonPath: jsonTag.Name, + Docstring: docstring, + } + outFields = append(outFields, outField) + } + } + + return outFields, nil +}