Skip to content

Commit

Permalink
dockerfile: generate lint rules documentation
Browse files Browse the repository at this point in the history
Signed-off-by: CrazyMax <[email protected]>
  • Loading branch information
crazy-max authored and tonistiigi committed Jun 6, 2024
1 parent 49b935f commit 7283eab
Show file tree
Hide file tree
Showing 6 changed files with 302 additions and 1 deletion.
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
16 changes: 15 additions & 1 deletion docker-bake.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -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" {
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
55 changes: 55 additions & 0 deletions frontend/dockerfile/docs/rules/file-consistent-command-casing.md
Original file line number Diff line number Diff line change
@@ -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)

49 changes: 49 additions & 0 deletions frontend/dockerfile/linter/docs/FileConsistentCommandCasing.md
Original file line number Diff line number Diff line change
@@ -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)
133 changes: 133 additions & 0 deletions frontend/dockerfile/linter/generate.go
Original file line number Diff line number Diff line change
@@ -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}`))
}
46 changes: 46 additions & 0 deletions hack/dockerfiles/docs-dockerfile.Dockerfile
Original file line number Diff line number Diff line change
@@ -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 <<EOT
set -ex
rsync -a /context/. .
cd frontend/dockerfile/linter
docsgen ./dist
mkdir /out
cp -r dist/* /out
EOT

FROM scratch AS update
COPY --from=gen /out /

FROM gen AS validate
RUN --mount=target=/context \
--mount=target=.,type=tmpfs <<EOT
set -e
rsync -a /context/. .
git add -A
rm -rf frontend/dockerfile/docs/rules/*
cp -rf /out/* ./frontend/dockerfile/docs/rules/
if [ -n "$(git status --porcelain -- frontend/dockerfile/docs/rules/)" ]; then
echo >&2 'ERROR: Dockerfile docs result differs. Please update with "make docs-dockerfile"'
git status --porcelain -- frontend/dockerfile/docs/rules/
exit 1
fi
EOT

0 comments on commit 7283eab

Please sign in to comment.