From 9c124a4e10c0d21ac6bc0a176ba9aa5af8c62d43 Mon Sep 17 00:00:00 2001 From: Michael Bolot Date: Fri, 28 Apr 2023 15:53:47 -0500 Subject: [PATCH] Adding docs generation and example docs files Adds logic to auto-generate a documentation directory and provides two docs files --- README.md | 8 ++ docs.md | 24 ++++ main.go | 2 +- pkg/codegen/cleanup/main.go | 4 + pkg/codegen/docs.go | 117 ++++++++++++++++++ pkg/codegen/main.go | 4 + .../v3/globalrole/GlobalRole.md | 8 ++ .../v3/roletemplate/RoleTemplate.md | 16 +++ 8 files changed, 182 insertions(+), 1 deletion(-) create mode 100644 docs.md create mode 100644 pkg/codegen/docs.go create mode 100644 pkg/resources/management.cattle.io/v3/globalrole/GlobalRole.md create mode 100644 pkg/resources/management.cattle.io/v3/roletemplate/RoleTemplate.md diff --git a/README.md b/README.md index bc7572fb..eaf6e0d3 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,14 @@ is also rejected. The Go web-server itself is configured by [dynamiclistener](https://github.com/rancher/dynamiclistener). It handles TLS certificates and the management of associated Secrets for secure communication of other Rancher components with the Webhook. +## Docs + +Documentation on each of the CRDs that are validated can be found in `docs.md`. It is recommended to review the [kubernetes docs on CRDs](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/#customresourcedefinitions) as well. + +Docs are added by creating a resource-specific readme in the directory of your mutator/validator (e.x. `pkg/resources/$GROUP/$GROUP_VERSION/$RESOURCE/$READABLE_RESOURCE.MD`). +These files should be named with a human-readable version of the CRD's name. +Running `go generate` will then aggregate these into the user-facing docs in the `docs.md` file. + ## Webhooks Rancher-Webhook is composed of multiple [WebhookHandlers](pkg/admission/admission.go) which is used when creating [ValidatingWebhooks](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#validatingwebhook-v1-admissionregistration-k8s-io) and [MutatingWebhooks](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#mutatingwebhook-v1-admissionregistration-k8s-io). diff --git a/docs.md b/docs.md new file mode 100644 index 00000000..73aea836 --- /dev/null +++ b/docs.md @@ -0,0 +1,24 @@ +# management.cattle.io/v3 + +## GlobalRole + +### Validation Checks +Note: all checks are bypassed if the GlobalRole is being deleted + +#### Escalation Prevention +Users can only change GlobalRoles which have less permissions than they do. This is to prevents privilege escalation. + +## RoleTemplate + +### Validation Checks +Note: all checks are bypassed if the RoleTemplate is being deleted + +#### Circular Reference +Circular references to webhooks (a inherits b, b inherits a) are not allowed. More specifically, if "roleTemplate1" is included in the `roleTemplateNames` of "roleTemplate2", then "roleTemplate2" must not be included in the `roleTemplateNames` of "roleTemplate1". This checks prevents the creation of roles whose end-state cannot be resolved. + +#### Rules Without Verbs +Rules without verbs are not peritted. The `rules` included in a roleTemplate are of the same type as the rules used by standard kubernetes RBAC types (such as `Roles` from `rbac.authorization.k8s.io/v1`). Because of this, they inherit the same restrictions as these types, including this one. + +#### Escalation Prevention +Users can only change RoleTemplates which have less permissions than they do. This prevents privilege escalation. + diff --git a/main.go b/main.go index 5b90bbdc..4b12ab75 100644 --- a/main.go +++ b/main.go @@ -1,5 +1,5 @@ //go:generate go run pkg/codegen/cleanup/main.go -//go:generate go run pkg/codegen/main.go pkg/codegen/template.go +//go:generate go run pkg/codegen/main.go pkg/codegen/template.go pkg/codegen/docs.go package main import ( diff --git a/pkg/codegen/cleanup/main.go b/pkg/codegen/cleanup/main.go index 68a6b25a..5938a452 100644 --- a/pkg/codegen/cleanup/main.go +++ b/pkg/codegen/cleanup/main.go @@ -10,4 +10,8 @@ func main() { if err := os.RemoveAll("./pkg/generated"); err != nil { logrus.Fatal(err) } + // if we don't have the docs file no need to clean it up + if err := os.Remove("./docs.md"); err != nil && !os.IsNotExist(err) { + logrus.Fatal(err) + } } diff --git a/pkg/codegen/docs.go b/pkg/codegen/docs.go new file mode 100644 index 00000000..9b0fc742 --- /dev/null +++ b/pkg/codegen/docs.go @@ -0,0 +1,117 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "golang.org/x/exp/slices" +) + +// docFileName defines the name of the files that will be aggregated into overall docs +const docFileExtension = ".md" + +type docFile struct { + content []byte + resource string + group string + version string +} + +func generateDocs(resourcesBaseDir, outputFilePath string) error { + outputFile, err := os.OpenFile(outputFilePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) + if err != nil { + return err + } + docFiles, err := getDocFiles(resourcesBaseDir) + if err != nil { + return fmt.Errorf("unable to create documentation: %w", err) + } + currentGroup := "" + for _, docFile := range docFiles { + newGroup := docFile.group + if newGroup != currentGroup { + // our group has changed, output a new group header + _, err = fmt.Fprintf(outputFile, "# %s/%s \n \n", docFile.group, docFile.version) + if err != nil { + return fmt.Errorf("unable to write group header for %s/%s: %w", docFile.group, docFile.version, err) + } + currentGroup = newGroup + } + + _, err = fmt.Fprintf(outputFile, "## %s \n\n", docFile.resource) + if err != nil { + return fmt.Errorf("unable to write resource header for %s: %w", docFile.resource, err) + } + + lines := strings.Split(string(docFile.content), "\n") + for i, line := range lines { + newLine := line + if i < len(lines)-1 { + // last line doesn't need a newLine re-added + newLine += "\n" + } + if strings.HasPrefix(line, "#") { + // this line is a markdown header. Since the group header is the top-level indent, indent this down one line + newLine = "#" + line + } + _, err := outputFile.WriteString(newLine) + if err != nil { + return fmt.Errorf("unable to write content for %s/%s.%s: %w", docFile.group, docFile.version, docFile.resource, err) + } + } + } + return nil +} + +// getDocFiles finds all markdown files recursively in resourcesBaseDir and converts them to docFiles. Returns in a sorted order, +// first by group, then by resourceName +func getDocFiles(baseDir string) ([]docFile, error) { + entries, err := os.ReadDir(baseDir) + if err != nil { + return nil, fmt.Errorf("unable to list entries in directory %s: %w", baseDir, err) + } + var docFiles []docFile + for _, entry := range entries { + entryPath := filepath.Join(baseDir, entry.Name()) + if entry.IsDir() { + subDocFiles, err := getDocFiles(entryPath) + if err != nil { + return nil, err + } + docFiles = append(docFiles, subDocFiles...) + } + if filepath.Ext(entry.Name()) == docFileExtension { + content, err := os.ReadFile(filepath.Join(baseDir, entry.Name())) + if err != nil { + return nil, fmt.Errorf("unable to read file content for %s: %w", entryPath, err) + } + var newDir, resource, version, group string + newDir, _ = filepath.Split(baseDir) + newDir, version = filepath.Split(newDir[:len(newDir)-1]) + newDir, group = filepath.Split(newDir[:len(newDir)-1]) + resource = strings.TrimSuffix(entry.Name(), docFileExtension) + if newDir == "" || resource == "" || version == "" || group == "" { + return nil, fmt.Errorf("unable to extract gvr from %s, got group %s, version %s, resource %s", baseDir, group, version, resource) + } + docFiles = append(docFiles, docFile{ + content: content, + resource: resource, + group: group, + version: version, + }) + } + } + // if the groups differ, sort based on the group. If the groups are the same, sort based on the resource + slices.SortFunc(docFiles, func(a, b docFile) bool { + if a.group < b.group { + return true + } else if a.group == b.group { + return a.resource < b.resource + } + return false + }) + + return docFiles, nil +} diff --git a/pkg/codegen/main.go b/pkg/codegen/main.go index a5c93cd9..c187a58c 100644 --- a/pkg/codegen/main.go +++ b/pkg/codegen/main.go @@ -24,6 +24,10 @@ type typeInfo struct { func main() { os.Unsetenv("GOPATH") + err := generateDocs("pkg/resources", "docs.md") + if err != nil { + panic(err) + } controllergen.Run(args.Options{ OutputPackage: "github.com/rancher/webhook/pkg/generated", Boilerplate: "scripts/boilerplate.go.txt", diff --git a/pkg/resources/management.cattle.io/v3/globalrole/GlobalRole.md b/pkg/resources/management.cattle.io/v3/globalrole/GlobalRole.md new file mode 100644 index 00000000..7c04c726 --- /dev/null +++ b/pkg/resources/management.cattle.io/v3/globalrole/GlobalRole.md @@ -0,0 +1,8 @@ +## Validation Checks + +Note: all checks are bypassed if the GlobalRole is being deleted + +### Escalation Prevention + +Users can only change GlobalRoles which have less permissions than they do. This is to prevents privilege escalation. + diff --git a/pkg/resources/management.cattle.io/v3/roletemplate/RoleTemplate.md b/pkg/resources/management.cattle.io/v3/roletemplate/RoleTemplate.md new file mode 100644 index 00000000..b500a949 --- /dev/null +++ b/pkg/resources/management.cattle.io/v3/roletemplate/RoleTemplate.md @@ -0,0 +1,16 @@ +## Validation Checks + +Note: all checks are bypassed if the RoleTemplate is being deleted + +### Circular Reference + +Circular references to webhooks (a inherits b, b inherits a) are not allowed. More specifically, if "roleTemplate1" is included in the `roleTemplateNames` of "roleTemplate2", then "roleTemplate2" must not be included in the `roleTemplateNames` of "roleTemplate1". This checks prevents the creation of roles whose end-state cannot be resolved. + +### Rules Without Verbs + +Rules without verbs are not peritted. The `rules` included in a roleTemplate are of the same type as the rules used by standard kubernetes RBAC types (such as `Roles` from `rbac.authorization.k8s.io/v1`). Because of this, they inherit the same restrictions as these types, including this one. + +### Escalation Prevention + +Users can only change RoleTemplates which have less permissions than they do. This prevents privilege escalation. +