From 7283eaba0d33546092d72ed6f451c21f02cd081d Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Tue, 4 Jun 2024 12:36:41 +0200 Subject: [PATCH] dockerfile: generate lint rules documentation Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- Makefile | 4 + docker-bake.hcl | 16 ++- .../rules/file-consistent-command-casing.md | 55 ++++++++ .../docs/FileConsistentCommandCasing.md | 49 +++++++ frontend/dockerfile/linter/generate.go | 133 ++++++++++++++++++ hack/dockerfiles/docs-dockerfile.Dockerfile | 46 ++++++ 6 files changed, 302 insertions(+), 1 deletion(-) create mode 100644 frontend/dockerfile/docs/rules/file-consistent-command-casing.md create mode 100644 frontend/dockerfile/linter/docs/FileConsistentCommandCasing.md create mode 100644 frontend/dockerfile/linter/generate.go create mode 100644 hack/dockerfiles/docs-dockerfile.Dockerfile diff --git a/Makefile b/Makefile index dc9f81293dfb..24ef2b1ff497 100644 --- a/Makefile +++ b/Makefile @@ -113,6 +113,10 @@ doctoc: docs: $(BUILDX_CMD) bake docs +.PHONY: docs-dockerfile +docs-dockerfile: + $(BUILDX_CMD) bake docs-dockerfile + .PHONY: mod-outdated mod-outdated: $(BUILDX_CMD) bake mod-outdated diff --git a/docker-bake.hcl b/docker-bake.hcl index d052d8c0cb9a..e439d502a1b0 100644 --- a/docker-bake.hcl +++ b/docker-bake.hcl @@ -126,7 +126,7 @@ target "integration-tests" { } group "validate" { - targets = ["lint", "validate-vendor", "validate-doctoc", "validate-generated-files", "validate-archutil", "validate-shfmt", "validate-docs"] + targets = ["lint", "validate-vendor", "validate-doctoc", "validate-generated-files", "validate-archutil", "validate-shfmt", "validate-docs", "validate-docs-dockerfile"] } target "lint" { @@ -211,6 +211,13 @@ target "validate-docs" { output = ["type=cacheonly"] } +target "validate-docs-dockerfile" { + inherits = ["_common"] + dockerfile = "./hack/dockerfiles/docs-dockerfile.Dockerfile" + target = "validate" + output = ["type=cacheonly"] +} + target "vendor" { inherits = ["_common"] dockerfile = "./hack/dockerfiles/vendor.Dockerfile" @@ -260,6 +267,13 @@ target "docs" { output = ["./docs"] } +target "docs-dockerfile" { + inherits = ["_common"] + dockerfile = "./hack/dockerfiles/docs-dockerfile.Dockerfile" + target = "update" + output = ["./frontend/dockerfile/docs/rules"] +} + target "mod-outdated" { inherits = ["_common"] dockerfile = "./hack/dockerfiles/vendor.Dockerfile" diff --git a/frontend/dockerfile/docs/rules/file-consistent-command-casing.md b/frontend/dockerfile/docs/rules/file-consistent-command-casing.md new file mode 100644 index 000000000000..89bef6236fa5 --- /dev/null +++ b/frontend/dockerfile/docs/rules/file-consistent-command-casing.md @@ -0,0 +1,55 @@ +--- +title: FileConsistentCommandCasing +description: All commands within the Dockerfile should use the same casing (either upper or lower) +--- + +Example warning: + +```text +Command 'foo' should match the case of the command majority (uppercase) +``` + +Instructions within a Dockerfile should have consistent casing through out the +entire files. Instructions are not case-sensitive, but the convention is to use +uppercase for instruction keywords to make it easier to distinguish keywords +from arguments. + +Whether you prefer instructions to be uppercase or lowercase, you should make +sure you use consistent casing to help improve readability of the Dockerfile. + +## Examples + +❌ Bad: mixing uppercase and lowercase + +```dockerfile +FROM alpine:latest AS builder +run apk --no-cache add build-base + +FROM builder AS build1 +copy source1.cpp source.cpp +``` + +✅ Good: all uppercase + +```dockerfile +FROM alpine:latest AS builder +RUN apk --no-cache add build-base + +FROM builder AS build1 +COPY source1.cpp source.cpp +``` + +✅ Good: all lowercase + +```dockerfile +from alpine:latest as builder +run apk --no-cache add build-base + +from builder as build1 +copy source1.cpp source.cpp +``` + +## Related errors + +- [`FromAsCasing`](./from-as-casing.md) + diff --git a/frontend/dockerfile/linter/docs/FileConsistentCommandCasing.md b/frontend/dockerfile/linter/docs/FileConsistentCommandCasing.md new file mode 100644 index 000000000000..ffeee81ac74d --- /dev/null +++ b/frontend/dockerfile/linter/docs/FileConsistentCommandCasing.md @@ -0,0 +1,49 @@ +Example warning: + +```text +Command 'foo' should match the case of the command majority (uppercase) +``` + +Instructions within a Dockerfile should have consistent casing through out the +entire files. Instructions are not case-sensitive, but the convention is to use +uppercase for instruction keywords to make it easier to distinguish keywords +from arguments. + +Whether you prefer instructions to be uppercase or lowercase, you should make +sure you use consistent casing to help improve readability of the Dockerfile. + +## Examples + +❌ Bad: mixing uppercase and lowercase + +```dockerfile +FROM alpine:latest AS builder +run apk --no-cache add build-base + +FROM builder AS build1 +copy source1.cpp source.cpp +``` + +✅ Good: all uppercase + +```dockerfile +FROM alpine:latest AS builder +RUN apk --no-cache add build-base + +FROM builder AS build1 +COPY source1.cpp source.cpp +``` + +✅ Good: all lowercase + +```dockerfile +from alpine:latest as builder +run apk --no-cache add build-base + +from builder as build1 +copy source1.cpp source.cpp +``` + +## Related errors + +- [`FromAsCasing`](./from-as-casing.md) diff --git a/frontend/dockerfile/linter/generate.go b/frontend/dockerfile/linter/generate.go new file mode 100644 index 000000000000..712d0c666bef --- /dev/null +++ b/frontend/dockerfile/linter/generate.go @@ -0,0 +1,133 @@ +//go:build ignore +// +build ignore + +package main + +import ( + "fmt" + "go/ast" + "go/parser" + "go/token" + "log" + "os" + "path" + "regexp" + "strings" + "text/template" + + "github.com/pkg/errors" +) + +type Rule struct { + Name string + Description string +} + +const tmplStr = `--- +title: {{.Rule.Name}} +description: {{.Rule.Description}} +--- + +{{.Content}} +` + +var destDir string + +func main() { + if len(os.Args) < 2 { + panic("Please provide a destination directory") + } + destDir = os.Args[1] + log.Printf("Destination directory: %s\n", destDir) + if err := run(destDir); err != nil { + panic(err) + } +} + +func run(destDir string) error { + if err := os.MkdirAll(destDir, 0700); err != nil { + return err + } + rules, err := listRules() + if err != nil { + return err + } + tmpl, err := template.New("rule").Parse(tmplStr) + if err != nil { + return err + } + for _, rule := range rules { + if ok, err := genRuleDoc(rule, tmpl); err != nil { + return errors.Wrapf(err, "Error generating docs for %s", rule.Name) + } else if ok { + log.Printf("Docs generated for %s\n", rule.Name) + } + } + return nil +} + +func genRuleDoc(rule Rule, tmpl *template.Template) (bool, error) { + mdfilename := fmt.Sprintf("docs/%s.md", rule.Name) + content, err := os.ReadFile(mdfilename) + if err != nil { + return false, err + } + outputfile, err := os.Create(path.Join(destDir, fmt.Sprintf("%s.md", camelToKebab(rule.Name)))) + if err != nil { + return false, err + } + defer outputfile.Close() + if err = tmpl.Execute(outputfile, struct { + Rule Rule + Content string + }{ + Rule: rule, + Content: string(content), + }); err != nil { + return false, err + } + return true, nil +} + +func listRules() ([]Rule, error) { + fset := token.NewFileSet() + node, err := parser.ParseFile(fset, "ruleset.go", nil, parser.ParseComments) + if err != nil { + return nil, err + } + var rules []Rule + ast.Inspect(node, func(n ast.Node) bool { + switch x := n.(type) { + case *ast.GenDecl: + for _, spec := range x.Specs { + if vSpec, ok := spec.(*ast.ValueSpec); ok { + rule := Rule{} + if cl, ok := vSpec.Values[0].(*ast.CompositeLit); ok { + for _, elt := range cl.Elts { + if kv, ok := elt.(*ast.KeyValueExpr); ok { + switch kv.Key.(*ast.Ident).Name { + case "Name": + if basicLit, ok := kv.Value.(*ast.BasicLit); ok { + rule.Name = strings.Trim(basicLit.Value, `"`) + } + case "Description": + if basicLit, ok := kv.Value.(*ast.BasicLit); ok { + rule.Description = strings.Trim(basicLit.Value, `"`) + } + } + } + } + } + rules = append(rules, rule) + } + } + } + return true + }) + return rules, nil +} + +func camelToKebab(s string) string { + var re = regexp.MustCompile(`([a-z])([A-Z])`) + return strings.ToLower(re.ReplaceAllString(s, `${1}-${2}`)) +} diff --git a/hack/dockerfiles/docs-dockerfile.Dockerfile b/hack/dockerfiles/docs-dockerfile.Dockerfile new file mode 100644 index 000000000000..49d42a7abba5 --- /dev/null +++ b/hack/dockerfiles/docs-dockerfile.Dockerfile @@ -0,0 +1,46 @@ +# syntax=docker/dockerfile:1 + +ARG GO_VERSION=1.21 +ARG ALPINE_VERSION=3.20 + +FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS golatest + +FROM golatest AS docsgen +WORKDIR /src +ENV CGO_ENABLED=0 +RUN --mount=target=. \ + --mount=target=/root/.cache,type=cache \ + --mount=target=/go/pkg/mod,type=cache \ + go build -mod=vendor -o /out/docsgen ./frontend/dockerfile/linter/generate.go + +FROM alpine AS gen +RUN apk add --no-cache rsync git +WORKDIR /src +COPY --from=docsgen /out/docsgen /usr/bin +RUN --mount=target=/context \ + --mount=target=.,type=tmpfs <&2 'ERROR: Dockerfile docs result differs. Please update with "make docs-dockerfile"' + git status --porcelain -- frontend/dockerfile/docs/rules/ + exit 1 +fi +EOT