├── .circleci └── config.yml ├── .github ├── pull_request_template.md ├── workflows │ ├── pre_commit_go.yaml │ ├── zz_generated.add-team-labels.yaml │ ├── zz_generated.add-to-project-board.yaml │ ├── zz_generated.create_release.yaml │ ├── zz_generated.create_release_pr.yaml │ ├── zz_generated.fix_vulnerabilities.yaml │ ├── zz_generated.gitleaks.yaml │ └── zz_generated.run_ossf_scorecard.yaml └── zz_generated.windows-code-signing.sh ├── .gitignore ├── .nancy-ignore ├── .nancy-ignore.generated ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── CODEOWNERS ├── DCO ├── Dockerfile ├── LICENSE ├── Makefile ├── Makefile.gen.go.mk ├── README.md ├── SECURITY.md ├── config.example.yaml ├── error └── error.go ├── go.mod ├── go.sum ├── main.go ├── main_test.go ├── pkg ├── annotations │ ├── annotations.go │ ├── annotations_test.go │ └── testdata │ │ └── aws.go ├── config │ ├── config.go │ ├── config_test.go │ ├── error.go │ └── testdata │ │ ├── config1.yaml │ │ └── config2.yaml ├── crd │ ├── crd.go │ ├── crd_test.go │ ├── error.go │ └── testdata │ │ └── awsclusterconfig.yaml ├── git │ └── git.go ├── jsonschema │ ├── jsonschema.go │ └── jsonschema_test.go └── output │ ├── error.go │ ├── output.go │ ├── output_test.go │ └── testdata │ ├── crd.template │ └── test_01.golden ├── renovate.json5 ├── templates └── crd.template └── testdata └── case1 ├── config.yaml ├── crd ├── crd1.yaml └── crd2.yaml ├── output.golden ├── output └── examples.example.giantswarm.io.md └── template.gotmpl /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | orbs: 3 | architect: giantswarm/architect@5.15.0 4 | 5 | workflows: 6 | build-workflow: 7 | jobs: 8 | - architect/go-build: 9 | name: go-build 10 | binary: crd-docs-generator 11 | filters: 12 | tags: 13 | only: /^v.*/ 14 | 15 | - architect/push-to-registries: 16 | context: architect 17 | name: push-to-registries 18 | requires: 19 | - go-build 20 | filters: 21 | tags: 22 | only: /^v.*/ 23 | branches: 24 | ignore: 25 | - main 26 | - master 27 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Checklist 2 | 3 | - [ ] Update changelog in CHANGELOG.md. 4 | -------------------------------------------------------------------------------- /.github/workflows/pre_commit_go.yaml: -------------------------------------------------------------------------------- 1 | # This file is maintained centrally in 2 | # https://github.com/giantswarm/github/blob/main/languages/go/pre_commit_go.yaml 3 | 4 | name: pre-commit 5 | 6 | on: 7 | pull_request: 8 | push: 9 | branches: [main] 10 | 11 | jobs: 12 | pre-commit: 13 | runs-on: ubuntu-24.04 14 | steps: 15 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 16 | - uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 17 | - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 18 | with: 19 | go-version: "1.24" 20 | - name: Install goimports 21 | run: | 22 | go install golang.org/x/tools/cmd/goimports@v0.31.0 23 | - name: Install golangci-lint 24 | uses: giantswarm/install-binary-action@c37eb401e5092993fc76d545030b1d1769e61237 # v3.0.0 25 | with: 26 | binary: golangci-lint 27 | version: "2.1.6" 28 | download_url: "https://github.com/golangci/golangci-lint/releases/download/v${version}/${binary}-${version}-linux-amd64.tar.gz" 29 | - uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1 30 | env: 31 | GOGC: "20" 32 | -------------------------------------------------------------------------------- /.github/workflows/zz_generated.add-team-labels.yaml: -------------------------------------------------------------------------------- 1 | name: Add appropriate labels to issue 2 | 3 | on: 4 | issues: 5 | types: [assigned] 6 | 7 | jobs: 8 | build_user_list: 9 | name: Get yaml config of GS users 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Get user-mapping 13 | run: | 14 | mkdir -p artifacts 15 | wget --header "Authorization: token ${{ secrets.ISSUE_AUTOMATION }}" \ 16 | -O artifacts/users.yaml \ 17 | https://raw.githubusercontent.com/giantswarm/github/main/tools/issue-automation/user-mapping.yaml 18 | - name: Upload Artifact 19 | uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 20 | with: 21 | name: users 22 | path: artifacts/users.yaml 23 | retention-days: 1 24 | 25 | add_label: 26 | name: Add team label when assigned 27 | runs-on: ubuntu-latest 28 | needs: build_user_list 29 | steps: 30 | - uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 31 | id: download-users 32 | with: 33 | name: users 34 | - name: Find team label based on user names 35 | run: | 36 | event_assignee=$(cat $GITHUB_EVENT_PATH | jq -r .assignee.login | tr '[:upper:]' '[:lower:]') 37 | echo "Issue assigned to: ${event_assignee}" 38 | 39 | TEAMS=$(cat ${{steps.download-users.outputs.download-path}}/users.yaml | tr '[:upper:]' '[:lower:]' | yq ".${event_assignee}.teams" -o csv | tr ',' ' ') 40 | 41 | echo "LABEL<> $GITHUB_ENV 42 | for team in ${TEAMS}; do 43 | echo "Team: ${team} | Label: team/${team}" 44 | echo "team/${team}" >> $GITHUB_ENV 45 | done 46 | echo "EOF" >> $GITHUB_ENV 47 | - name: Apply label to issue 48 | if: ${{ env.LABEL != '' && env.LABEL != 'null' && env.LABEL != null }} 49 | uses: actions-ecosystem/action-add-labels@bd52874380e3909a1ac983768df6976535ece7f8 # v1.1.3 50 | with: 51 | github_token: ${{ secrets.ISSUE_AUTOMATION }} 52 | labels: | 53 | ${{ env.LABEL }} 54 | -------------------------------------------------------------------------------- /.github/workflows/zz_generated.add-to-project-board.yaml: -------------------------------------------------------------------------------- 1 | name: Add Issue to Project when assigned 2 | 3 | on: 4 | issues: 5 | types: 6 | - assigned 7 | - labeled 8 | 9 | jobs: 10 | build_user_list: 11 | name: Get yaml config of GS users 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Get user-mapping 15 | run: | 16 | mkdir -p artifacts 17 | wget --header "Authorization: token ${{ secrets.ISSUE_AUTOMATION }}" \ 18 | -O artifacts/users.yaml \ 19 | https://raw.githubusercontent.com/giantswarm/github/main/tools/issue-automation/user-mapping.yaml 20 | - name: Upload Artifact 21 | uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 22 | with: 23 | name: users 24 | path: artifacts/users.yaml 25 | retention-days: 1 26 | - name: Get label-mapping 27 | run: | 28 | mkdir -p artifacts 29 | wget --header "Authorization: token ${{ secrets.ISSUE_AUTOMATION }}" \ 30 | -O artifacts/labels.yaml \ 31 | https://raw.githubusercontent.com/giantswarm/github/main/tools/issue-automation/label-mapping.yaml 32 | - name: Upload Artifact 33 | uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 34 | with: 35 | name: labels 36 | path: artifacts/labels.yaml 37 | retention-days: 1 38 | 39 | add_to_personal_board: 40 | name: Add issue to personal board 41 | runs-on: ubuntu-latest 42 | needs: build_user_list 43 | if: github.event.action == 'assigned' 44 | steps: 45 | - uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 46 | id: download-users 47 | with: 48 | name: users 49 | - name: Find personal board based on user names 50 | run: | 51 | event_assignee=$(cat $GITHUB_EVENT_PATH | jq -r .assignee.login | tr '[:upper:]' '[:lower:]') 52 | echo "Issue assigned to: ${event_assignee}" 53 | 54 | BOARD=($(cat ${{steps.download-users.outputs.download-path}}/users.yaml | tr '[:upper:]' '[:lower:]' | yq ".${event_assignee}.personalboard")) 55 | echo "Personal board URL: ${BOARD}" 56 | 57 | echo "BOARD=${BOARD}" >> $GITHUB_ENV 58 | - name: Add issue to personal board 59 | if: ${{ env.BOARD != 'null' && env.BOARD != '' && env.BOARD != null }} 60 | uses: actions/add-to-project@9bfe908f2eaa7ba10340b31e314148fcfe6a2458 # v1.0.1 61 | with: 62 | project-url: ${{ env.BOARD }} 63 | github-token: ${{ secrets.ISSUE_AUTOMATION }} 64 | 65 | add_to_team_board: 66 | name: Add issue to team board 67 | runs-on: ubuntu-latest 68 | needs: build_user_list 69 | if: github.event.action == 'labeled' 70 | steps: 71 | - uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 72 | id: download-labels 73 | with: 74 | name: labels 75 | - name: Find team board based on label 76 | run: | 77 | event_label=$(cat $GITHUB_EVENT_PATH | jq -r .label.name | tr '[:upper:]' '[:lower:]') 78 | echo "Issue labelled with: ${event_label}" 79 | 80 | BOARD=($(cat ${{steps.download-labels.outputs.download-path}}/labels.yaml | tr '[:upper:]' '[:lower:]' | yq ".[\"${event_label}\"].projectboard")) 81 | echo "Team board URL: ${BOARD}" 82 | 83 | echo "BOARD=${BOARD}" >> $GITHUB_ENV 84 | - name: Add issue to team board 85 | if: ${{ env.BOARD != 'null' && env.BOARD != '' && env.BOARD != null }} 86 | uses: actions/add-to-project@9bfe908f2eaa7ba10340b31e314148fcfe6a2458 # v1.0.1 87 | with: 88 | project-url: ${{ env.BOARD }} 89 | github-token: ${{ secrets.ISSUE_AUTOMATION }} 90 | -------------------------------------------------------------------------------- /.github/workflows/zz_generated.create_release.yaml: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT. Generated with: 2 | # 3 | # devctl 4 | # 5 | # https://github.com/giantswarm/devctl/blob/e88bfdc523e849f242bd022e78522750d9ea53dd/pkg/gen/input/workflows/internal/file/create_release.yaml.template 6 | # 7 | name: Create Release 8 | on: 9 | push: 10 | branches: 11 | - 'legacy' 12 | - 'main' 13 | - 'master' 14 | - 'release-v*.*.x' 15 | # "!" negates previous positive patterns so it has to be at the end. 16 | - '!release-v*.x.x' 17 | jobs: 18 | debug_info: 19 | name: Debug info 20 | runs-on: ubuntu-22.04 21 | steps: 22 | - name: Print github context JSON 23 | run: | 24 | cat <> $GITHUB_OUTPUT 55 | - name: Checkout code 56 | if: ${{ steps.get_version.outputs.version != '' }} 57 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 58 | - name: Get project.go path 59 | id: get_project_go_path 60 | if: ${{ steps.get_version.outputs.version != '' }} 61 | run: | 62 | path='./pkg/project/project.go' 63 | if [[ ! -f $path ]] ; then 64 | path='' 65 | fi 66 | echo "path=\"$path\"" 67 | echo "path=${path}" >> $GITHUB_OUTPUT 68 | - name: Check if reference version 69 | id: ref_version 70 | env: 71 | COMMIT_MESSAGE: ${{ github.event.head_commit.message }} 72 | run: | 73 | title=$(echo -n "${COMMIT_MESSAGE}" | head -1) 74 | if echo "${title}" | grep -qE '^release v[0-9]+\.[0-9]+\.[0-9]+([.-][^ .-][^ ]*)?( \(#[0-9]+\))?$' ; then 75 | version=$(echo "${title}" | cut -d ' ' -f 2) 76 | fi 77 | version=$(echo "${title}" | cut -d ' ' -f 2) 78 | version="${version#v}" # Strip "v" prefix. 79 | refversion=false 80 | if [[ "${version}" =~ ^[0-9]+.[0-9]+.[0-9]+-[0-9]+$ ]]; then 81 | refversion=true 82 | fi 83 | echo "refversion =\"${refversion}\"" 84 | echo "refversion=${refversion}" >> $GITHUB_OUTPUT 85 | update_project_go: 86 | name: Update project.go 87 | runs-on: ubuntu-22.04 88 | if: ${{ needs.gather_facts.outputs.version != '' && needs.gather_facts.outputs.project_go_path != '' && needs.gather_facts.outputs.ref_version != 'true' }} 89 | needs: 90 | - gather_facts 91 | steps: 92 | - name: Install architect 93 | uses: giantswarm/install-binary-action@c37eb401e5092993fc76d545030b1d1769e61237 # v3.0.0 94 | with: 95 | binary: "architect" 96 | version: "6.14.1" 97 | - name: Install semver 98 | uses: giantswarm/install-binary-action@c37eb401e5092993fc76d545030b1d1769e61237 # v3.0.0 99 | with: 100 | binary: "semver" 101 | version: "3.2.0" 102 | download_url: "https://github.com/fsaintjacques/${binary}-tool/archive/${version}.tar.gz" 103 | tarball_binary_path: "*/src/${binary}" 104 | smoke_test: "${binary} --version" 105 | - name: Checkout code 106 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 107 | - name: Update project.go 108 | id: update_project_go 109 | env: 110 | branch: "${{ github.ref }}-version-bump" 111 | run: | 112 | git checkout -b ${{ env.branch }} 113 | file="${{ needs.gather_facts.outputs.project_go_path }}" 114 | version="${{ needs.gather_facts.outputs.version }}" 115 | new_version="$(semver bump patch $version)-dev" 116 | echo "version=\"$version\" new_version=\"$new_version\"" 117 | echo "new_version=${new_version}" >> $GITHUB_OUTPUT 118 | sed -Ei "s/(version[[:space:]]*=[[:space:]]*)\"${version}\"/\1\"${new_version}\"/" $file 119 | if git diff --exit-code $file ; then 120 | echo "error: no changes in \"$file\"" >&2 121 | exit 1 122 | fi 123 | - name: Set up git identity 124 | run: | 125 | git config --local user.email "dev@giantswarm.io" 126 | git config --local user.name "taylorbot" 127 | - name: Commit changes 128 | run: | 129 | file="${{ needs.gather_facts.outputs.project_go_path }}" 130 | git add $file 131 | git commit -m "Bump version to ${{ steps.update_project_go.outputs.new_version }}" 132 | - name: Push changes 133 | env: 134 | REMOTE_REPO: "https://${{ github.actor }}:${{ secrets.TAYLORBOT_GITHUB_ACTION }}@github.com/${{ github.repository }}.git" 135 | branch: "${{ github.ref }}-version-bump" 136 | run: | 137 | git push "${REMOTE_REPO}" HEAD:${{ env.branch }} 138 | - name: Create PR 139 | env: 140 | GITHUB_TOKEN: "${{ secrets.TAYLORBOT_GITHUB_ACTION }}" 141 | base: "${{ github.ref }}" 142 | branch: "${{ github.ref }}-version-bump" 143 | version: "${{ needs.gather_facts.outputs.version }}" 144 | title: "Bump version to ${{ steps.update_project_go.outputs.new_version }}" 145 | run: | 146 | gh pr create --title "${{ env.title }}" --body "" --base ${{ env.base }} --head ${{ env.branch }} --reviewer ${{ github.actor }} 147 | - name: Enable auto-merge for PR 148 | env: 149 | GITHUB_TOKEN: "${{ secrets.TAYLORBOT_GITHUB_ACTION }}" 150 | base: "${{ github.ref }}" 151 | branch: "${{ github.ref }}-version-bump" 152 | version: "${{ needs.gather_facts.outputs.version }}" 153 | title: "Bump version to ${{ steps.update_project_go.outputs.new_version }}" 154 | run: | 155 | gh pr merge --auto --squash "${{ env.branch }}" || echo "::warning::Auto-merge not allowed. Please adjust the repository settings." 156 | create_release: 157 | name: Create release 158 | runs-on: ubuntu-22.04 159 | needs: 160 | - gather_facts 161 | if: ${{ needs.gather_facts.outputs.version }} 162 | outputs: 163 | upload_url: ${{ steps.create_gh_release.outputs.upload_url }} 164 | steps: 165 | - name: Checkout code 166 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 167 | with: 168 | ref: ${{ github.sha }} 169 | - name: Ensure correct version in project.go 170 | if: ${{ needs.gather_facts.outputs.project_go_path != '' && needs.gather_facts.outputs.ref_version != 'true' }} 171 | run: | 172 | file="${{ needs.gather_facts.outputs.project_go_path }}" 173 | version="${{ needs.gather_facts.outputs.version }}" 174 | grep -qE "version[[:space:]]*=[[:space:]]*\"$version\"" $file 175 | - name: Get Changelog Entry 176 | id: changelog_reader 177 | uses: mindsers/changelog-reader-action@32aa5b4c155d76c94e4ec883a223c947b2f02656 # v2.2.3 178 | with: 179 | version: ${{ needs.gather_facts.outputs.version }} 180 | path: ./CHANGELOG.md 181 | - name: Set up git identity 182 | run: | 183 | git config --local user.email "dev@giantswarm.io" 184 | git config --local user.name "taylorbot" 185 | - name: Create tag 186 | run: | 187 | version="${{ needs.gather_facts.outputs.version }}" 188 | git tag "v$version" ${{ github.sha }} 189 | - name: Push tag 190 | env: 191 | REMOTE_REPO: "https://${{ github.actor }}:${{ secrets.TAYLORBOT_GITHUB_ACTION }}@github.com/${{ github.repository }}.git" 192 | run: | 193 | git push "${REMOTE_REPO}" --tags 194 | - name: Create release 195 | id: create_gh_release 196 | uses: ncipollo/release-action@440c8c1cb0ed28b9f43e4d1d670870f059653174 # v1.16.0 197 | env: 198 | GITHUB_TOKEN: "${{ secrets.TAYLORBOT_GITHUB_ACTION }}" 199 | with: 200 | body: ${{ steps.changelog_reader.outputs.changes }} 201 | tag: "v${{ needs.gather_facts.outputs.version }}" 202 | 203 | create-release-branch: 204 | name: Create release branch 205 | runs-on: ubuntu-22.04 206 | needs: 207 | - gather_facts 208 | if: ${{ needs.gather_facts.outputs.version }} 209 | steps: 210 | - name: Install semver 211 | uses: giantswarm/install-binary-action@c37eb401e5092993fc76d545030b1d1769e61237 # v3.0.0 212 | with: 213 | binary: "semver" 214 | version: "3.0.0" 215 | download_url: "https://github.com/fsaintjacques/${binary}-tool/archive/${version}.tar.gz" 216 | tarball_binary_path: "*/src/${binary}" 217 | smoke_test: "${binary} --version" 218 | - name: Check out the repository 219 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 220 | with: 221 | fetch-depth: 0 # Clone the whole history, not just the most recent commit. 222 | - name: Fetch all tags and branches 223 | run: "git fetch --all" 224 | - name: Create long-lived release branch 225 | run: | 226 | current_version="${{ needs.gather_facts.outputs.version }}" 227 | parent_version="$(git describe --tags --abbrev=0 HEAD^ || true)" 228 | parent_version="${parent_version#v}" # Strip "v" prefix. 229 | 230 | if [[ -z "$parent_version" ]] ; then 231 | echo "Unable to find a parent tag version. No branch to create." 232 | exit 0 233 | fi 234 | 235 | echo "current_version=$current_version parent_version=$parent_version" 236 | 237 | current_major=$(semver get major $current_version) 238 | current_minor=$(semver get minor $current_version) 239 | parent_major=$(semver get major $parent_version) 240 | parent_minor=$(semver get minor $parent_version) 241 | echo "current_major=$current_major current_minor=$current_minor parent_major=$parent_major parent_minor=$parent_minor" 242 | 243 | if [[ $current_major -gt $parent_major ]] ; then 244 | echo "Current tag is a new major version" 245 | elif [[ $current_major -eq $parent_major ]] && [[ $current_minor -gt $parent_minor ]] ; then 246 | echo "Current tag is a new minor version" 247 | else 248 | echo "Current tag is not a new major or minor version. Nothing to do here." 249 | exit 0 250 | fi 251 | 252 | release_branch="release-v${parent_major}.${parent_minor}.x" 253 | echo "release_branch=$release_branch" 254 | 255 | if git rev-parse --verify $release_branch ; then 256 | echo "Release branch $release_branch already exists. Nothing to do here." 257 | exit 0 258 | fi 259 | 260 | git branch $release_branch HEAD^ 261 | git push origin $release_branch 262 | 263 | create_and_upload_build_artifacts: 264 | name: Create and upload build artifacts 265 | runs-on: ubuntu-22.04 266 | strategy: 267 | fail-fast: false 268 | matrix: 269 | platform: 270 | - darwin-amd64 271 | - linux-amd64 272 | - darwin-arm64 273 | - linux-arm64 274 | - windows-amd64 275 | env: 276 | GITHUB_TOKEN: "${{ secrets.TAYLORBOT_GITHUB_ACTION }}" 277 | GO_VERSION: 1.21.3 278 | ARTIFACT_DIR: bin-dist 279 | TAG: v${{ needs.gather_facts.outputs.version }} 280 | CODE_SIGNING_CERT_BUNDLE_BASE64: ${{ secrets.CODE_SIGNING_CERT_BUNDLE_BASE64 }} 281 | CODE_SIGNING_CERT_BUNDLE_PASSWORD: ${{ secrets.CODE_SIGNING_CERT_BUNDLE_PASSWORD }} 282 | needs: 283 | - create_release 284 | - gather_facts 285 | steps: 286 | - name: Install architect 287 | uses: giantswarm/install-binary-action@c37eb401e5092993fc76d545030b1d1769e61237 # v3.0.0 288 | with: 289 | binary: "architect" 290 | version: "6.14.1" 291 | - name: Set up Go ${{ env.GO_VERSION }} 292 | uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 293 | with: 294 | go-version: ${{ env.GO_VERSION }} 295 | - name: Checkout code 296 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 297 | with: 298 | ref: ${{ env.TAG }} 299 | # devctl specific, we need the whole git history of branch `main` for generating the correct urls in header templates 300 | - name: Fetch main branch 301 | run: "git fetch origin main --unshallow" 302 | - name: Create ${{ matrix.platform }} package 303 | run: make package-${{ matrix.platform }} 304 | - name: Specify package file name based on platform 305 | run: | 306 | if [[ "${{ matrix.platform }}" == "windows-amd64" ]]; then 307 | echo "FILE_NAME=${{ github.event.repository.name }}-${{ env.TAG }}-${{ matrix.platform }}.zip" >> $GITHUB_ENV 308 | else 309 | echo "FILE_NAME=${{ github.event.repository.name }}-${{ env.TAG }}-${{ matrix.platform }}.tar.gz" >> $GITHUB_ENV 310 | fi 311 | - name: Add ${{ matrix.platform }} package to release 312 | env: 313 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 314 | run: | 315 | gh release upload ${{ env.TAG }} \ 316 | ${{ env.ARTIFACT_DIR }}/${{ env.FILE_NAME }}#${{ env.FILE_NAME }} 317 | -------------------------------------------------------------------------------- /.github/workflows/zz_generated.create_release_pr.yaml: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT. Generated with: 2 | # 3 | # devctl 4 | # 5 | # https://github.com/giantswarm/devctl/blob/d9db43c55d8732e2fe786a8ad446727877325f61/pkg/gen/input/workflows/internal/file/create_release_pr.yaml.template 6 | # 7 | name: Create Release PR 8 | on: 9 | push: 10 | branches: 11 | - 'legacy#release#v*.*.*' 12 | - 'main#release#v*.*.*' 13 | - 'main#release#major' 14 | - 'main#release#minor' 15 | - 'main#release#patch' 16 | - 'master#release#v*.*.*' 17 | - 'master#release#major' 18 | - 'master#release#minor' 19 | - 'master#release#patch' 20 | - 'release#v*.*.*' 21 | - 'release#major' 22 | - 'release#minor' 23 | - 'release#patch' 24 | - 'release-v*.*.x#release#v*.*.*' 25 | # "!" negates previous positive patterns so it has to be at the end. 26 | - '!release-v*.x.x#release#v*.*.*' 27 | workflow_call: 28 | inputs: 29 | branch: 30 | required: true 31 | type: string 32 | 33 | jobs: 34 | publish: 35 | uses: giantswarm/github-workflows/.github/workflows/create-release-pr.yaml@b58c650fb0f34858ba9f2c1e45eee3392ce0cf26 36 | with: 37 | branch: ${{ inputs.branch }} 38 | secrets: 39 | TAYLORBOT_GITHUB_ACTION: ${{ secrets.TAYLORBOT_GITHUB_ACTION }} 40 | -------------------------------------------------------------------------------- /.github/workflows/zz_generated.fix_vulnerabilities.yaml: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT. Generated with: 2 | # 3 | # devctl 4 | # 5 | # https://github.com/giantswarm/devctl/blob/8b210b8821f90f70a98d396d357855c0e389795d/pkg/gen/input/workflows/internal/file/fix_vulnerabilities.yaml.template 6 | # 7 | name: Fix Vulnerabilities 8 | on: 9 | schedule: 10 | - cron: '0 9 * * 1-5' 11 | workflow_dispatch: 12 | inputs: 13 | branch: 14 | description: "Branch on which to fix vulnerabilities" 15 | required: true 16 | type: string 17 | workflow_call: 18 | inputs: 19 | branch: 20 | required: true 21 | type: string 22 | jobs: 23 | gather_facts: 24 | name: Gather facts 25 | runs-on: ubuntu-22.04 26 | outputs: 27 | branch: ${{ steps.gather_facts.outputs.branch }} 28 | skip : ${{ steps.gather_facts.outputs.skip }} 29 | steps: 30 | - name: Checkout code 31 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 32 | with: 33 | ref: ${{ inputs.branch || github.ref }} 34 | - name: Gather facts 35 | id: gather_facts 36 | run: | 37 | head="${{ inputs.branch || github.ref }}" 38 | branch="${{ github.ref_name }}" 39 | 40 | echo "branch=${branch}" >> $GITHUB_OUTPUT 41 | 42 | head="${head#refs/heads/}" # Strip "refs/heads/" prefix. 43 | echo "head=${head}" >> $GITHUB_OUTPUT 44 | 45 | # Skip if there are no go mod files 46 | if [[ ! -e go.mod ]] && [[ ! -e go.sum ]]; then 47 | skip=true 48 | echo "There are no go mod files in the repo, skipping" 49 | else 50 | skip=false 51 | fi 52 | 53 | echo "skip=${skip}" >> $GITHUB_OUTPUT 54 | echo "head=\"$head\" branch=\"$branch\" skip=\"$skip\"" 55 | run_nancy_fixer: 56 | name: Fix vulnerabilities with nancy-fixer 57 | runs-on: ubuntu-22.04 58 | needs: 59 | - gather_facts 60 | if: ${{ needs.gather_facts.outputs.skip != 'true' }} 61 | steps: 62 | - name: Generate a token 63 | id: generate_token 64 | uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6 65 | with: 66 | app-id: ${{ secrets.HERALD_APP_ID }} 67 | private-key: ${{ secrets.HERALD_APP_KEY }} 68 | - name: Checkout code 69 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 70 | with: 71 | token: ${{ steps.generate_token.outputs.token }} 72 | persist-credentials: false 73 | ref: ${{ needs.gather_facts.outputs.branch }} 74 | - name: Create new branch 75 | id: create_branch 76 | run: | 77 | branch="remediate-vulnerabilities-${{ needs.gather_facts.outputs.branch }}" 78 | echo "branch=${branch}" >> $GITHUB_OUTPUT 79 | git checkout -b "${branch}" 80 | git pull origin "${branch}" || true 81 | - name: Run nancy-fixer fix 82 | uses: docker://gsoci.azurecr.io/giantswarm/nancy-fixer:0.5.1 83 | timeout-minutes: 20 84 | - name: Set up git identity 85 | run: | 86 | git config --local user.email "149080493+heraldbot[bot]@users.noreply.github.com" 87 | git config --local user.name "HeraldBot[bot]" 88 | - name: Commit new files 89 | id: commit_changes 90 | run: | 91 | git add -A 92 | if git diff-index --quiet HEAD; then 93 | echo "No changes found" 94 | skip=true 95 | else 96 | git commit -m "Remediate Nancy findings" 97 | skip=false 98 | fi 99 | echo "skip=${skip}" >> $GITHUB_OUTPUT 100 | - name: Push changes 101 | if: "${{ steps.commit_changes.outputs.skip != 'true' }}" 102 | env: 103 | remote_repo: "https://${{ github.actor }}:${{ steps.generate_token.outputs.token }}@github.com/${{ github.repository }}.git" 104 | run: | 105 | git push "${remote_repo}" HEAD:"${{ steps.create_branch.outputs.branch }}" 106 | - name: Create PR 107 | env: 108 | GITHUB_TOKEN: "${{ steps.generate_token.outputs.token }}" 109 | if: "${{ steps.commit_changes.outputs.skip != 'true' }}" 110 | run: | 111 | gh pr create --title "Remediate Nancy findings on ${{ needs.gather_facts.outputs.branch }}" --body "Fix Nancy findings on branch ${{ needs.gather_facts.outputs.branch }}" --head ${{ steps.create_branch.outputs.branch }} --base "${{ needs.gather_facts.outputs.branch }}" 112 | gh pr merge --auto --squash 113 | -------------------------------------------------------------------------------- /.github/workflows/zz_generated.gitleaks.yaml: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT. Generated with: 2 | # 3 | # devctl 4 | # 5 | # https://github.com/giantswarm/devctl/blob/adf10b36c877e395e4f428c1ccc9e38d517510a5/pkg/gen/input/workflows/internal/file/gitleaks.yaml.template 6 | # 7 | name: gitleaks 8 | 9 | on: 10 | - pull_request 11 | 12 | jobs: 13 | publish: 14 | uses: giantswarm/github-workflows/.github/workflows/gitleaks.yaml@b58c650fb0f34858ba9f2c1e45eee3392ce0cf26 15 | -------------------------------------------------------------------------------- /.github/workflows/zz_generated.run_ossf_scorecard.yaml: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT. Generated with: 2 | # 3 | # devctl 4 | # 5 | # https://github.com/giantswarm/devctl/blob/36d8e107c8f41a8f7aa6aeeb49db5ee1771a4f15/pkg/gen/input/workflows/internal/file/run_ossf_scorecard.yaml.template 6 | # 7 | 8 | # This workflow uses actions that are not certified by GitHub. They are provided 9 | # by a third-party and are governed by separate terms of service, privacy 10 | # policy, and support documentation. 11 | 12 | name: Scorecard supply-chain security 13 | on: 14 | # For Branch-Protection check. Only the default branch is supported. See 15 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection 16 | branch_protection_rule: 17 | # To guarantee Maintained check is occasionally updated. See 18 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained 19 | schedule: 20 | - cron: '15 15 15 * *' 21 | push: 22 | branches: [ "main", "master" ] 23 | workflow_dispatch: {} 24 | 25 | # Declare default permissions as read only. 26 | permissions: read-all 27 | 28 | jobs: 29 | analysis: 30 | name: Scorecard analysis 31 | runs-on: ubuntu-24.04 32 | permissions: 33 | # Needed to upload the results to code-scanning dashboard. 34 | security-events: write 35 | # Needed to publish results and get a badge (see publish_results below). 36 | id-token: write 37 | # Uncomment the permissions below if installing in a private repository. 38 | # contents: read 39 | # actions: read 40 | 41 | steps: 42 | - name: "Checkout code" 43 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 44 | with: 45 | persist-credentials: false 46 | 47 | - name: "Run analysis" 48 | uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1 49 | with: 50 | results_file: results.sarif 51 | results_format: sarif 52 | # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: 53 | # - you want to enable the Branch-Protection check on a *public* repository, or 54 | # - you are installing Scorecard on a *private* repository 55 | # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. 56 | # repo_token: ${{ secrets.SCORECARD_TOKEN }} 57 | 58 | # Public repositories: 59 | # - Publish results to OpenSSF REST API for easy access by consumers 60 | # - Allows the repository to include the Scorecard badge. 61 | # - See https://github.com/ossf/scorecard-action#publishing-results. 62 | # For private repositories: 63 | # - `publish_results` will always be set to `false`, regardless 64 | # of the value entered here. 65 | publish_results: true 66 | 67 | # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF 68 | # format to the repository Actions tab. 69 | - name: "Upload artifact" 70 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 71 | with: 72 | name: SARIF file 73 | path: results.sarif 74 | retention-days: 5 75 | 76 | # Upload the results to GitHub's code scanning dashboard. 77 | - name: "Upload to code-scanning" 78 | uses: github/codeql-action/upload-sarif@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 79 | with: 80 | sarif_file: results.sarif 81 | -------------------------------------------------------------------------------- /.github/zz_generated.windows-code-signing.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # DO NOT EDIT. Generated with: 4 | # 5 | # devctl 6 | # 7 | # https://github.com/giantswarm/devctl/blob/c1b5bc102e28d8733b8a2a9732af9801430aefff/pkg/gen/input/makefile/internal/file/windows-code-signing.sh.template 8 | # 9 | 10 | APPLICATION=$1 11 | VERSION=$2 12 | 13 | SIGNCODE_UTIL=gsoci.azurecr.io/giantswarm/signcode-util:1.1.1 14 | 15 | echo "APPLICATION=${APPLICATION}" 16 | echo "VERSION=${VERSION}" 17 | echo "PWD=${PWD}" 18 | 19 | NO_CODE_SIGNING="Skipping Windows binary signing. In order to create a signed Windows binary, set the environment variables CODE_SIGNING_CERT_BUNDLE_PASSWORD and CODE_SIGNING_CERT_BUNDLE_BASE64." 20 | 21 | if [ "${CODE_SIGNING_CERT_BUNDLE_PASSWORD}" = "" ]; then 22 | echo "Variable CODE_SIGNING_CERT_BUNDLE_PASSWORD not set. ${NO_CODE_SIGNING}" 23 | exit 0 24 | fi; 25 | 26 | if [ "${CODE_SIGNING_CERT_BUNDLE_BASE64}" = "" ]; then 27 | echo "Variable CODE_SIGNING_CERT_BUNDLE_BASE64 not set. ${NO_CODE_SIGNING}" 28 | exit 0 29 | fi; 30 | 31 | echo "Signing the Windows binary" 32 | 33 | mkdir -p certs 34 | 35 | echo "${CODE_SIGNING_CERT_BUNDLE_BASE64}" | base64 -d > certs/code-signing.p12 36 | 37 | mv "${APPLICATION}-v${VERSION}-windows-amd64.exe" "${APPLICATION}-v${VERSION}-windows-amd64-unsigned.exe" 38 | 39 | docker pull --quiet ${SIGNCODE_UTIL} 40 | 41 | docker run --rm \ 42 | -v "${PWD}/certs:/mnt/certs" \ 43 | -v "${PWD}:/mnt/binaries" \ 44 | ${SIGNCODE_UTIL} \ 45 | sign \ 46 | -pkcs12 /mnt/certs/code-signing.p12 \ 47 | -n "Giant Swarm CLI tool ${APPLICATION}" \ 48 | -i "https://github.com/giantswarm/${APPLICATION}" \ 49 | -t http://timestamp.digicert.com -verbose \ 50 | -in "/mnt/binaries/${APPLICATION}-v${VERSION}-windows-amd64-unsigned.exe" \ 51 | -out "/mnt/binaries/${APPLICATION}-v${VERSION}-windows-amd64.exe" \ 52 | -pass "${CODE_SIGNING_CERT_BUNDLE_PASSWORD}" 53 | 54 | echo "Verifying the signed binary" 55 | 56 | docker run --rm \ 57 | -v "${PWD}:/mnt/binaries" \ 58 | ${SIGNCODE_UTIL} \ 59 | verify \ 60 | "/mnt/binaries/${APPLICATION}-v${VERSION}-windows-amd64.exe" 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /output 2 | /crd-docs-generator 3 | -------------------------------------------------------------------------------- /.nancy-ignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giantswarm/crd-docs-generator/27555aa07634dcc044539f5a0aaac3ed2204d4a5/.nancy-ignore -------------------------------------------------------------------------------- /.nancy-ignore.generated: -------------------------------------------------------------------------------- 1 | # This file is generated by https://github.com/giantswarm/github 2 | # Repository specific ignores should be added to .nancy-ignore 3 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # This file is maintained centrally at 2 | # https://github.com/giantswarm/github/blob/main/languages/go/.pre-commit-config.yaml 3 | 4 | minimum_pre_commit_version: '2.17' 5 | repos: 6 | # shell scripts 7 | - repo: https://github.com/detailyang/pre-commit-shell 8 | rev: 1.0.5 9 | hooks: 10 | - id: shell-lint 11 | args: [ --format=json ] 12 | exclude: ".*\\.template" 13 | 14 | - repo: https://github.com/pre-commit/pre-commit-hooks 15 | rev: v5.0.0 16 | hooks: 17 | - id: check-added-large-files 18 | - id: check-merge-conflict 19 | - id: check-shebang-scripts-are-executable 20 | - id: detect-private-key 21 | - id: end-of-file-fixer 22 | exclude: ".*testdata/.*" 23 | - id: mixed-line-ending 24 | - id: trailing-whitespace 25 | exclude: ".*testdata/.*" 26 | 27 | - repo: https://github.com/dnephin/pre-commit-golang 28 | rev: v0.5.1 29 | hooks: 30 | - id: go-fmt 31 | - id: go-mod-tidy 32 | - id: golangci-lint 33 | args: 34 | - -E=gosec 35 | - -E=goconst 36 | - -E=govet 37 | # timeout is needed for CI 38 | - --timeout=300s 39 | # List all issues found 40 | - --max-same-issues=0 41 | - --max-issues-per-linter=0 42 | - id: go-imports 43 | args: [ -local, github.com/giantswarm/crd-docs-generator ] 44 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ### Changed 11 | 12 | - Switched YAML parser from `gopkg.in/yaml.v3` to `github.com/goccy/go-yaml`. 13 | 14 | ## [0.11.4] - 2025-04-23 15 | 16 | ### Changed 17 | 18 | - Dependency updates 19 | - Support the kube-builder generated CRD sample pattern that does not renders the CRD domain (e.g. giantswarm.io) into the filename. 20 | 21 | ## [0.11.3] - 2025-03-06 22 | 23 | ## [0.11.2] - 2024-12-17 24 | 25 | ### Changed 26 | 27 | - No functional changes compared to 0.11.1, just a `go.mod` cleanup that should enable installation via `go install`. 28 | 29 | ## [0.11.1] - 2024-02-21 30 | 31 | ### Changed 32 | 33 | - Use base images from `gsoci.azurecr.io`. 34 | - Numerous dependency updates. 35 | 36 | ## [0.11.0] - 2022-10-28 37 | 38 | ### Changed 39 | 40 | - **Potentially breaking:** Annotation doc comments have a new format, because go-fmt v1.19 breaks our old formatting. For an example look at the file /pkg/annotations/testdata/aws.go. 41 | 42 | ## [0.10.0] - 2022-03-22 43 | 44 | - **Potentially breaking:** Your templates might have to be adapted like done in [this commit](https://github.com/giantswarm/crd-docs-generator/pull/98/files?file-filters%5B%5D=.template&show-viewed-files=true). 45 | - The order of versions in a CRD output page is now guaranteed. 46 | - The output path is now configurable via the config file directive `output_path`. 47 | - If the output folder does not exist, it will be created. 48 | 49 | ## [0.9.0] - 2022-01-26 50 | 51 | ### Changed 52 | 53 | Breaking: source paths are now configured in the config file. 54 | 55 | Three new configuration keys have been introduced to configure paths per source_repository: 56 | 57 | - `crd_paths`: paths to search for CRD YAML files. 58 | - `cr_paths`: paths to search for example CR YAML files. 59 | - `annotations_paths`: paths to search for Go files defining annotations. 60 | 61 | All expect array values. Paths are relative to the source repo root. See the `config.example.yaml` file to learn how to use the keys. 62 | 63 | ## [0.8.0] - 2021-12-09 64 | 65 | - Breaking: rename `.APIVersion` template field to `.CRDVersion`. 66 | - Support multiple source repositories 67 | - Update jwt-go dependency 68 | - Refactoring to enable better testing 69 | - Add test for output package 70 | - Improve logging 71 | 72 | ## [0.7.1] - 2021-07-21 73 | 74 | - Pass through deprecation metadata from apiextensions to output (https://github.com/giantswarm/crd-docs-generator/pull/46) 75 | 76 | ## [0.7.0] - 2021-07-19 77 | 78 | - Use metadata from the apiextensions repository (https://github.com/giantswarm/crd-docs-generator/pull/44) 79 | 80 | ## [0.6.1] - 2021-05-14 81 | 82 | - Add support for another apiextensions repo path `/helm/**/upstream.yaml`. 83 | - Parse multiple CRDs from a single YAML file. 84 | 85 | ## [0.6.0] - 2021-05-14 86 | 87 | - Change path where to look for CRD YAML in giantswarm/apiextensions from `/config/crd/v1` to `/config/crd`. 88 | 89 | ## [0.5.0] - 2021-02-18 90 | 91 | - Breaking: Remove configuration option `commit_reference`, add command line flag `--commit-reference` for the same purpose instead. 92 | 93 | ## [0.4.0] - 2021-02-12 94 | 95 | - Add flag `--template` to specify the template path 96 | - Render annotations with documentation in CRDs. 97 | 98 | ## [0.3.0] - 2021-02-02 99 | 100 | - Don't try to add syntax highlighting. Use tripple backtick instead. 101 | - Update dependencies. 102 | 103 | ## [0.2.3] - 2021-01-14 104 | 105 | - Change name "Management Cluster API" to "Management API". 106 | 107 | ## [0.2.2] - 2021-01-08 108 | 109 | - Add more terminology changes and add aliases for redirects after URL changes. 110 | 111 | ## [0.2.1] - 2021-01-08 112 | 113 | - Change name "Control Plane Kubernetes API" to "Management Cluster API". 114 | 115 | ## [0.2.0] - 2020-12-03 116 | 117 | - Remove date field from front matter of generated pages, as it's no longer needed. 118 | 119 | ## [0.1.2] - 2020-10-05 120 | 121 | - Remove whitespace around 'Required'. 122 | 123 | ## [0.1.1] - 2020-06-29 124 | 125 | - Add a link target to every attribute name headline. 126 | 127 | ## [0.1.0] - 2020-05-06 128 | 129 | - Add blacklisting feature to skip certain CRDs that should not get documented 130 | - Move example CR above property details 131 | - Fix a headline tag 132 | - Adapt CRD input path to match latest changes in the apiextensions repo 133 | - Refactor: move functions into services 134 | - Use config file for settings instead of flags 135 | - Switch CI from architect to architect-orb 136 | 137 | [Unreleased]: https://github.com/giantswarm/crd-docs-generator/compare/v0.11.4...HEAD 138 | [0.11.4]: https://github.com/giantswarm/crd-docs-generator/compare/v0.11.3...v0.11.4 139 | [0.11.3]: https://github.com/giantswarm/crd-docs-generator/compare/v0.11.2...v0.11.3 140 | [0.11.2]: https://github.com/giantswarm/crd-docs-generator/compare/v0.11.1...v0.11.2 141 | [0.11.1]: https://github.com/giantswarm/crd-docs-generator/compare/v0.11.0...v0.11.1 142 | [0.11.0]: https://github.com/giantswarm/crd-docs-generator/compare/v0.10.0...v0.11.0 143 | [0.10.0]: https://github.com/giantswarm/crd-docs-generator/compare/v0.9.0...v0.10.0 144 | [0.9.0]: https://github.com/giantswarm/crd-docs-generator/compare/v0.8.0...v0.9.0 145 | [0.8.0]: https://github.com/giantswarm/crd-docs-generator/compare/v0.7.1...v0.8.0 146 | [0.7.1]: https://github.com/giantswarm/crd-docs-generator/compare/v0.7.0...v0.7.1 147 | [0.7.0]: https://github.com/giantswarm/crd-docs-generator/compare/v0.6.1...v0.7.0 148 | [0.6.1]: https://github.com/giantswarm/crd-docs-generator/compare/v0.6.0...v0.6.1 149 | [0.6.0]: https://github.com/giantswarm/crd-docs-generator/compare/v0.5.0...v0.6.0 150 | [0.5.0]: https://github.com/giantswarm/crd-docs-generator/compare/v0.4.0...v0.5.0 151 | [0.4.0]: https://github.com/giantswarm/crd-docs-generator/compare/v0.3.0...v0.4.0 152 | [0.3.0]: https://github.com/giantswarm/crd-docs-generator/compare/v0.2.3...v0.3.0 153 | [0.2.3]: https://github.com/giantswarm/crd-docs-generator/compare/v0.2.2...v0.2.3 154 | [0.2.2]: https://github.com/giantswarm/crd-docs-generator/compare/v0.2.1...v0.2.2 155 | [0.2.1]: https://github.com/giantswarm/crd-docs-generator/compare/v0.2.0...v0.2.1 156 | [0.2.0]: https://github.com/giantswarm/crd-docs-generator/compare/v0.1.2...v0.2.0 157 | [0.1.2]: https://github.com/giantswarm/crd-docs-generator/compare/v0.1.1...v0.1.2 158 | [0.1.1]: https://github.com/giantswarm/crd-docs-generator/compare/v0.1.0...v0.1.1 159 | [0.1.0]: https://github.com/giantswarm/crd-docs-generator/releases/tag/v0.1.0 160 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # generated by giantswarm/github actions - changes will be overwritten 2 | * @giantswarm/team-honeybadger 3 | -------------------------------------------------------------------------------- /DCO: -------------------------------------------------------------------------------- 1 | Developer Certificate of Origin 2 | Version 1.1 3 | 4 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 5 | 660 York Street, Suite 102, 6 | San Francisco, CA 94110 USA 7 | 8 | Everyone is permitted to copy and distribute verbatim copies of this 9 | license document, but changing it is not allowed. 10 | 11 | 12 | Developer's Certificate of Origin 1.1 13 | 14 | By making a contribution to this project, I certify that: 15 | 16 | (a) The contribution was created in whole or in part by me and I 17 | have the right to submit it under the open source license 18 | indicated in the file; or 19 | 20 | (b) The contribution is based upon previous work that, to the best 21 | of my knowledge, is covered under an appropriate open source 22 | license and I have the right under that license to submit that 23 | work with modifications, whether created in whole or in part 24 | by me, under the same open source license (unless I am 25 | permitted to submit under a different license), as indicated 26 | in the file; or 27 | 28 | (c) The contribution was provided directly to me by some other 29 | person who certified (a), (b) or (c) and I have not modified 30 | it. 31 | 32 | (d) I understand and agree that this project and the contribution 33 | are public and that a record of the contribution (including all 34 | personal information I submit with it, including my sign-off) is 35 | maintained indefinitely and may be redistributed consistent with 36 | this project or the open source license(s) involved. 37 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM gsoci.azurecr.io/giantswarm/alpine:3.22.0 2 | 3 | RUN apk add --no-cache ca-certificates git 4 | 5 | COPY . /opt/crd-docs-generator 6 | 7 | WORKDIR /opt/crd-docs-generator 8 | 9 | ENTRYPOINT ["/opt/crd-docs-generator/crd-docs-generator"] 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2016 - 2025 Giant Swarm GmbH 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT. Generated with: 2 | # 3 | # devctl 4 | # 5 | # https://github.com/giantswarm/devctl/blob/6a704f7e2a8b0f09e82b5bab88f17971af849711/pkg/gen/input/makefile/internal/file/Makefile.template 6 | # 7 | 8 | include Makefile.*.mk 9 | 10 | ##@ General 11 | 12 | # The help target prints out all targets with their descriptions organized 13 | # beneath their categories. The categories are represented by '##@' and the 14 | # target descriptions by '##'. The awk commands is responsible for reading the 15 | # entire set of makefiles included in this invocation, looking for lines of the 16 | # file as xyz: ## something, and then pretty-format the target and help. Then, 17 | # if there's a line with ##@ something, that gets pretty-printed as a category. 18 | # More info on the usage of ANSI control characters for terminal formatting: 19 | # https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters 20 | # More info on the awk command: 21 | # http://linuxcommand.org/lc3_adv_awk.php 22 | 23 | .PHONY: help 24 | help: ## Display this help. 25 | @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z%\\\/_0-9-]+:.*?##/ { printf " \033[36m%-20s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) 26 | -------------------------------------------------------------------------------- /Makefile.gen.go.mk: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT. Generated with: 2 | # 3 | # devctl 4 | # 5 | # https://github.com/giantswarm/devctl/blob/fb22684ec4540f6b602968f01b4845bfb7713ee2/pkg/gen/input/makefile/internal/file/Makefile.gen.go.mk.template 6 | # 7 | 8 | PACKAGE_DIR := ./bin-dist 9 | 10 | APPLICATION := $(shell go list -m | cut -d '/' -f 3) 11 | BUILDTIMESTAMP := $(shell date -u '+%FT%TZ') 12 | GITSHA1 := $(shell git rev-parse --verify HEAD) 13 | MODULE := $(shell go list -m) 14 | OS := $(shell go env GOOS) 15 | SOURCES := $(shell find . -name '*.go') 16 | VERSION := $(shell architect project version) 17 | ifeq ($(OS), linux) 18 | EXTLDFLAGS := -static 19 | endif 20 | LDFLAGS ?= -w -linkmode 'auto' -extldflags '$(EXTLDFLAGS)' \ 21 | -X '$(shell go list -m)/pkg/project.buildTimestamp=${BUILDTIMESTAMP}' \ 22 | -X '$(shell go list -m)/pkg/project.gitSHA=${GITSHA1}' 23 | 24 | .DEFAULT_GOAL := build 25 | 26 | ##@ Go 27 | 28 | .PHONY: build build-darwin build-darwin-64 build-linux build-linux-arm64 build-windows-amd64 29 | build: $(APPLICATION) ## Builds a local binary. 30 | @echo "====> $@" 31 | build-darwin: $(APPLICATION)-darwin ## Builds a local binary for darwin/amd64. 32 | @echo "====> $@" 33 | build-darwin-arm64: $(APPLICATION)-darwin-arm64 ## Builds a local binary for darwin/arm64. 34 | @echo "====> $@" 35 | build-linux: $(APPLICATION)-linux ## Builds a local binary for linux/amd64. 36 | @echo "====> $@" 37 | build-linux-arm64: $(APPLICATION)-linux-arm64 ## Builds a local binary for linux/arm64. 38 | @echo "====> $@" 39 | build-windows-amd64: $(APPLICATION)-windows-amd64.exe ## Builds a local binary for windows/amd64. 40 | @echo "====> $@" 41 | 42 | $(APPLICATION): $(APPLICATION)-v$(VERSION)-$(OS)-amd64 43 | @echo "====> $@" 44 | cp -a $< $@ 45 | 46 | $(APPLICATION)-darwin: $(APPLICATION)-v$(VERSION)-darwin-amd64 47 | @echo "====> $@" 48 | cp -a $< $@ 49 | 50 | $(APPLICATION)-darwin-arm64: $(APPLICATION)-v$(VERSION)-darwin-arm64 51 | @echo "====> $@" 52 | cp -a $< $@ 53 | 54 | $(APPLICATION)-linux: $(APPLICATION)-v$(VERSION)-linux-amd64 55 | @echo "====> $@" 56 | cp -a $< $@ 57 | 58 | $(APPLICATION)-linux-arm64: $(APPLICATION)-v$(VERSION)-linux-arm64 59 | @echo "====> $@" 60 | cp -a $< $@ 61 | 62 | $(APPLICATION)-windows-amd64.exe: $(APPLICATION)-v$(VERSION)-windows-amd64.exe 63 | @echo "====> $@" 64 | cp -a $< $@ 65 | 66 | $(APPLICATION)-v$(VERSION)-%-amd64: $(SOURCES) 67 | @echo "====> $@" 68 | CGO_ENABLED=0 GOOS=$* GOARCH=amd64 go build -trimpath -ldflags "$(LDFLAGS)" -o $@ . 69 | 70 | $(APPLICATION)-v$(VERSION)-%-arm64: $(SOURCES) 71 | @echo "====> $@" 72 | CGO_ENABLED=0 GOOS=$* GOARCH=arm64 go build -trimpath -ldflags "$(LDFLAGS)" -o $@ . 73 | 74 | $(APPLICATION)-v$(VERSION)-windows-amd64.exe: $(SOURCES) 75 | @echo "====> $@" 76 | CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -trimpath -ldflags "$(LDFLAGS)" -o $@ . 77 | 78 | .PHONY: package-darwin-amd64 package-darwin-arm64 package-linux-amd64 package-linux-arm64 package-windows-amd64 79 | package-darwin-amd64: $(PACKAGE_DIR)/$(APPLICATION)-v$(VERSION)-darwin-amd64.tar.gz ## Prepares a packaged darwin/amd64 version. 80 | @echo "====> $@" 81 | package-darwin-arm64: $(PACKAGE_DIR)/$(APPLICATION)-v$(VERSION)-darwin-arm64.tar.gz ## Prepares a packaged darwin/arm64 version. 82 | @echo "====> $@" 83 | package-linux-amd64: $(PACKAGE_DIR)/$(APPLICATION)-v$(VERSION)-linux-amd64.tar.gz ## Prepares a packaged linux/amd64 version. 84 | @echo "====> $@" 85 | package-linux-arm64: $(PACKAGE_DIR)/$(APPLICATION)-v$(VERSION)-linux-arm64.tar.gz ## Prepares a packaged linux/arm64 version. 86 | @echo "====> $@" 87 | package-windows-amd64: $(PACKAGE_DIR)/$(APPLICATION)-v$(VERSION)-windows-amd64.zip ## Prepares a packaged windows/amd64 version. 88 | @echo "====> $@" 89 | 90 | $(PACKAGE_DIR)/$(APPLICATION)-v$(VERSION)-windows-amd64.zip: DIR=$(PACKAGE_DIR)/$(APPLICATION)-v$(VERSION)-windows-amd64 91 | $(PACKAGE_DIR)/$(APPLICATION)-v$(VERSION)-windows-amd64.zip: $(APPLICATION)-v$(VERSION)-windows-amd64.exe 92 | @echo "====> $@" 93 | /bin/sh .github/zz_generated.windows-code-signing.sh $(APPLICATION) $(VERSION) 94 | @echo "Creating directory $(DIR)" 95 | mkdir -p $(DIR) 96 | cp $< $(DIR)/$(APPLICATION).exe 97 | cp README.md LICENSE $(DIR) 98 | cd ./bin-dist && zip $(APPLICATION)-v$(VERSION)-windows-amd64.zip $(APPLICATION)-v$(VERSION)-windows-amd64/* 99 | rm -rf $(DIR) 100 | rm -rf $< 101 | 102 | $(PACKAGE_DIR)/$(APPLICATION)-v$(VERSION)-%-amd64.tar.gz: DIR=$(PACKAGE_DIR)/$< 103 | $(PACKAGE_DIR)/$(APPLICATION)-v$(VERSION)-%-amd64.tar.gz: $(APPLICATION)-v$(VERSION)-%-amd64 104 | @echo "====> $@" 105 | mkdir -p $(DIR) 106 | cp $< $(DIR)/$(APPLICATION) 107 | cp README.md LICENSE $(DIR) 108 | tar -C $(PACKAGE_DIR) -cvzf $(PACKAGE_DIR)/$<.tar.gz $< 109 | rm -rf $(DIR) 110 | rm -rf $< 111 | 112 | $(PACKAGE_DIR)/$(APPLICATION)-v$(VERSION)-%-arm64.tar.gz: DIR=$(PACKAGE_DIR)/$< 113 | $(PACKAGE_DIR)/$(APPLICATION)-v$(VERSION)-%-arm64.tar.gz: $(APPLICATION)-v$(VERSION)-%-arm64 114 | @echo "====> $@" 115 | mkdir -p $(DIR) 116 | cp $< $(DIR)/$(APPLICATION) 117 | cp README.md LICENSE $(DIR) 118 | tar -C $(PACKAGE_DIR) -cvzf $(PACKAGE_DIR)/$<.tar.gz $< 119 | rm -rf $(DIR) 120 | rm -rf $< 121 | 122 | .PHONY: install 123 | install: ## Install the application. 124 | @echo "====> $@" 125 | go install -ldflags "$(LDFLAGS)" . 126 | 127 | .PHONY: run 128 | run: ## Runs go run main.go. 129 | @echo "====> $@" 130 | go run -ldflags "$(LDFLAGS)" -race . 131 | 132 | .PHONY: clean 133 | clean: ## Cleans the binary. 134 | @echo "====> $@" 135 | rm -f $(APPLICATION)* 136 | go clean 137 | 138 | .PHONY: imports 139 | imports: ## Runs goimports. 140 | @echo "====> $@" 141 | goimports -local $(MODULE) -w . 142 | 143 | .PHONY: lint 144 | lint: ## Runs golangci-lint. 145 | @echo "====> $@" 146 | golangci-lint run -E gosec -E goconst --timeout=15m ./... 147 | 148 | .PHONY: nancy 149 | nancy: ## Runs nancy (requires v1.0.37 or newer). 150 | @echo "====> $@" 151 | CGO_ENABLED=0 go list -json -deps ./... | nancy sleuth --skip-update-check --quiet --exclude-vulnerability-file ./.nancy-ignore --additional-exclude-vulnerability-files ./.nancy-ignore.generated 152 | 153 | .PHONY: test 154 | test: ## Runs go test with default values. 155 | @echo "====> $@" 156 | go test -ldflags "$(LDFLAGS)" -race ./... 157 | 158 | .PHONY: build-docker 159 | build-docker: build-linux ## Builds docker image to registry. 160 | @echo "====> $@" 161 | cp -a $(APPLICATION)-linux $(APPLICATION) 162 | docker build -t ${APPLICATION}:${VERSION} . 163 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CircleCI](https://circleci.com/gh/giantswarm/crd-docs-generator/tree/master.svg?style=svg&circle-token=2847f4b99edcb9776cbd8ee622b294eb96bfd55f)](https://circleci.com/gh/giantswarm/crd-docs-generator/tree/master) 2 | 3 | # crd-docs-generator 4 | 5 | Generates schema reference documentation for Kubernetes Custom Resource Definitions (CRDs). 6 | 7 | This tool is built to generate our [Management API CRD schema reference](https://docs.giantswarm.io/ui-api/management-api/crd/). 8 | 9 | The generated output consists of Markdown files packed with HTML. By itself, this does not provide a fully readable and user-friendly set of documentation pages. Instead it relies on the HUGO website context, as the [giantswarm/docs](https://github.com/giantswarm/docs) repository, to provide an index page and useful styling. 10 | 11 | ## Assumptions/Prerequisites 12 | 13 | This tool relies on: 14 | 15 | - CRDs being defined in public source repositories in YAML format. 16 | - CRDs providing an OpenAPIv3 validation schema 17 | - either in the `.spec.validation` section of a CRD containg only one version 18 | - or in the `.spec.versions[*].schema` position of a CRD containing multiple versions 19 | - OpenAPIv3 schemas containing `description` attributes for every property. 20 | - The topmost `description` value explaining the CRD itself. (For a CRD containing multiple versions, the first `description` found is used as such.) 21 | - CR examples to be found in the source repository/repositories as one example per YAML file. 22 | 23 | ## Usage 24 | 25 | ### Docker 26 | 27 | The generator can be executed in Docker using a command like this: 28 | 29 | ```nohighlight 30 | docker run \ 31 | -v $PWD/path/to/output-folder:/opt/crd-docs-generator/output \ 32 | -v $PWD:/opt/crd-docs-generator/config \ 33 | gsoci.azurecr.io/giantswarm/crd-docs-generator:0.11.0 \ 34 | --config /opt/crd-docs-generator/config/config.example.yaml 35 | ``` 36 | 37 | Here, the tag `0.11.0` is the version number of the crd-docs-generator release you're going to use. See our GitHub releases for available tags. 38 | 39 | The volume mapping defines where the generated output will land. 40 | 41 | ### Development 42 | 43 | With Go installed and this repository cloned, you can exetute the program like this: 44 | 45 | ```nohighlight 46 | go run main.go --config config.example.yaml 47 | ``` 48 | 49 | See the `config.example.yaml` file for an idea how to configure your source repositories. 50 | 51 | ## TODO 52 | 53 | - Parse template only once instead of for every CRD 54 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | Please visit for information on reporting security issues. 6 | -------------------------------------------------------------------------------- /config.example.yaml: -------------------------------------------------------------------------------- 1 | # This is an example configuration file 2 | # used for development and testing purposes. 3 | # 4 | # For the effective config used in production for 5 | # docs.giantswarm.io, see 6 | # https://github.com/giantswarm/docs/blob/main/scripts/update-crd-reference/config.yaml 7 | 8 | template_path: templates/crd.template 9 | 10 | output_path: output 11 | 12 | # List of repositories containing CRDs. 13 | # Sequence is important here if several repos provide the same CRD (based on the full name). 14 | # In this case, the first repo "wins". Subsequent repos adding the same CRD will result in a 15 | # warning. 16 | source_repositories: 17 | - url: https://github.com/giantswarm/apiextensions-application 18 | organization: giantswarm 19 | short_name: apiextensions-application 20 | commit_reference: v0.3.0 21 | crd_paths: 22 | - config/crd 23 | cr_paths: 24 | - docs/cr 25 | metadata: 26 | appcatalogs.application.giantswarm.io: 27 | owner: 28 | - https://github.com/orgs/giantswarm/teams/team-honeybadger 29 | topics: 30 | - apps 31 | deprecation: 32 | replaced_by: 33 | full_name: catalogs.application.giantswarm.io 34 | short_name: Catalog 35 | appcatalogentries.application.giantswarm.io: 36 | owner: 37 | - https://github.com/orgs/giantswarm/teams/team-honeybadger 38 | topics: 39 | - apps 40 | apps.application.giantswarm.io: 41 | owner: 42 | - https://github.com/orgs/giantswarm/teams/team-honeybadger 43 | topics: 44 | - apps 45 | catalogs.application.giantswarm.io: 46 | owner: 47 | - https://github.com/orgs/giantswarm/teams/team-honeybadger 48 | topics: 49 | - apps 50 | charts.application.giantswarm.io: 51 | owner: 52 | - https://github.com/orgs/giantswarm/teams/team-honeybadger 53 | topics: 54 | - apps 55 | - url: https://github.com/giantswarm/config-controller 56 | organization: giantswarm 57 | short_name: config-controller 58 | commit_reference: v0.5.1 59 | crd_paths: 60 | - config/crd 61 | cr_paths: 62 | - docs/cr 63 | metadata: 64 | configs.core.giantswarm.io: 65 | owner: 66 | - https://github.com/orgs/giantswarm/teams/team-honeybadger 67 | topics: 68 | - apps 69 | - managementcluster 70 | - url: https://github.com/giantswarm/silence-operator 71 | organization: giantswarm 72 | short_name: silence-operator 73 | commit_reference: v0.4.0 74 | crd_paths: 75 | - config/crd 76 | cr_paths: 77 | - docs/cr 78 | metadata: 79 | silences.monitoring.giantswarm.io: 80 | owner: 81 | - https://github.com/orgs/giantswarm/teams/team-atlas 82 | topics: 83 | - managementcluster 84 | - url: https://github.com/giantswarm/release-operator 85 | organization: giantswarm 86 | short_name: release-operator 87 | commit_reference: v3.0.1 88 | crd_paths: 89 | - config/crd 90 | cr_paths: 91 | - docs/cr 92 | metadata: 93 | releases.release.giantswarm.io: 94 | owner: 95 | - https://github.com/orgs/giantswarm/teams/team-honeybadger 96 | topics: 97 | - managementcluster 98 | - workloadcluster 99 | - url: https://github.com/giantswarm/apiextensions 100 | organization: giantswarm 101 | short_name: apiextensions 102 | commit_reference: v3.39.0 103 | annotations_paths: 104 | - pkg/annotation 105 | crd_paths: 106 | - config/crd 107 | - helm 108 | cr_paths: 109 | - docs/cr 110 | metadata: 111 | awsclusters.infrastructure.cluster.x-k8s.io: 112 | owner: 113 | - https://github.com/orgs/giantswarm/teams/team-phoenix 114 | hidden: true 115 | awsclusters.infrastructure.giantswarm.io: 116 | owner: 117 | - https://github.com/orgs/giantswarm/teams/team-phoenix 118 | provider: 119 | - aws 120 | topics: 121 | - workloadcluster 122 | deprecation: 123 | info: This CRD will be removed once Cluster API resources are used for all AWS workload clusters. 124 | awsclusterconfigs.core.giantswarm.io: 125 | owner: 126 | - https://github.com/orgs/giantswarm/teams/team-phoenix 127 | hidden: true 128 | awsclustercontrolleridentities.infrastructure.cluster.x-k8s.io: 129 | owner: 130 | - https://github.com/orgs/giantswarm/teams/team-phoenix 131 | hidden: true 132 | awsclusterroleidentities.infrastructure.cluster.x-k8s.io: 133 | owner: 134 | - https://github.com/orgs/giantswarm/teams/team-phoenix 135 | hidden: true 136 | awsclusterstaticidentities.infrastructure.cluster.x-k8s.io: 137 | owner: 138 | - https://github.com/orgs/giantswarm/teams/team-phoenix 139 | hidden: true 140 | awsconfigs.provider.giantswarm.io: 141 | owner: 142 | - https://github.com/orgs/giantswarm/teams/team-phoenix 143 | hidden: true 144 | awscontrolplanes.infrastructure.giantswarm.io: 145 | owner: 146 | - https://github.com/orgs/giantswarm/teams/team-phoenix 147 | provider: 148 | - aws 149 | topics: 150 | - workloadcluster 151 | deprecation: 152 | info: This CRD will be removed once Cluster API resources are used for all AWS workload clusters. 153 | awsfargateprofiles.infrastructure.cluster.x-k8s.io: 154 | owner: 155 | - https://github.com/orgs/giantswarm/teams/team-phoenix 156 | hidden: true 157 | awsmachinedeployments.infrastructure.giantswarm.io: 158 | owner: 159 | - https://github.com/orgs/giantswarm/teams/team-phoenix 160 | provider: 161 | - aws 162 | topics: 163 | - workloadcluster 164 | awsmachinepools.infrastructure.cluster.x-k8s.io: 165 | owner: 166 | - https://github.com/orgs/giantswarm/teams/team-phoenix 167 | hidden: true 168 | awsmachines.infrastructure.cluster.x-k8s.io: 169 | owner: 170 | - https://github.com/orgs/giantswarm/teams/team-phoenix 171 | hidden: true 172 | awsmachinetemplates.infrastructure.cluster.x-k8s.io: 173 | owner: 174 | - https://github.com/orgs/giantswarm/teams/team-phoenix 175 | hidden: true 176 | awsmanagedclusters.infrastructure.cluster.x-k8s.io: 177 | owner: 178 | - https://github.com/orgs/giantswarm/teams/team-phoenix 179 | hidden: true 180 | awsmanagedcontrolplanes.controlplane.cluster.x-k8s.io: 181 | owner: 182 | - https://github.com/orgs/giantswarm/teams/team-phoenix 183 | hidden: true 184 | awsmanagedmachinepools.infrastructure.cluster.x-k8s.io: 185 | owner: 186 | - https://github.com/orgs/giantswarm/teams/team-phoenix 187 | hidden: true 188 | azureassignedidentities.aadpodidentity.k8s.io: 189 | owner: 190 | - https://github.com/orgs/giantswarm/teams/team-phoenix 191 | hidden: true 192 | azureclusteridentities.infrastructure.cluster.x-k8s.io: 193 | owner: 194 | - https://github.com/orgs/giantswarm/teams/team-phoenix 195 | hidden: true 196 | azureclusters.infrastructure.cluster.x-k8s.io: 197 | owner: 198 | - https://github.com/orgs/giantswarm/teams/team-phoenix 199 | provider: 200 | - azure 201 | topics: 202 | - workloadcluster 203 | azureclusterconfigs.core.giantswarm.io: 204 | owner: 205 | - https://github.com/orgs/giantswarm/teams/team-phoenix 206 | hidden: true 207 | azureconfigs.provider.giantswarm.io: 208 | owner: 209 | - https://github.com/orgs/giantswarm/teams/team-phoenix 210 | hidden: true 211 | azureidentities.aadpodidentity.k8s.io: 212 | owner: 213 | - https://github.com/orgs/giantswarm/teams/team-phoenix 214 | hidden: true 215 | azureidentitybindings.aadpodidentity.k8s.io: 216 | owner: 217 | - https://github.com/orgs/giantswarm/teams/team-phoenix 218 | hidden: true 219 | azuremachinepools.exp.infrastructure.cluster.x-k8s.io: 220 | owner: 221 | - https://github.com/orgs/giantswarm/teams/team-phoenix 222 | provider: 223 | - azure 224 | topics: 225 | - workloadcluster 226 | azuremachines.infrastructure.cluster.x-k8s.io: 227 | owner: 228 | - https://github.com/orgs/giantswarm/teams/team-phoenix 229 | provider: 230 | - azure 231 | topics: 232 | - workloadcluster 233 | azuremachinetemplates.infrastructure.cluster.x-k8s.io: 234 | owner: 235 | - https://github.com/orgs/giantswarm/teams/team-phoenix 236 | hidden: true 237 | azuremanagedclusters.exp.infrastructure.cluster.x-k8s.io: 238 | owner: 239 | - https://github.com/orgs/giantswarm/teams/team-phoenix 240 | hidden: true 241 | azuremanagedcontrolplanes.exp.infrastructure.cluster.x-k8s.io: 242 | owner: 243 | - https://github.com/orgs/giantswarm/teams/team-phoenix 244 | hidden: true 245 | azuremanagedmachinepools.exp.infrastructure.cluster.x-k8s.io: 246 | owner: 247 | - https://github.com/orgs/giantswarm/teams/team-phoenix 248 | hidden: true 249 | azurepodidentityexceptions.aadpodidentity.k8s.io: 250 | owner: 251 | - https://github.com/orgs/giantswarm/teams/team-phoenix 252 | hidden: true 253 | certconfigs.core.giantswarm.io: 254 | owner: 255 | - https://github.com/orgs/giantswarm/teams/team-cabbage 256 | topics: 257 | - managementcluster 258 | - workloadcluster 259 | chartconfigs.core.giantswarm.io: 260 | hidden: true 261 | clusterclasses.cluster.x-k8s.io: 262 | hidden: true 263 | clusterresourcesetbindings.addons.cluster.x-k8s.io: 264 | hidden: true 265 | clusterresourcesets.addons.cluster.x-k8s.io: 266 | hidden: true 267 | clusters.core.giantswarm.io: 268 | hidden: true 269 | clusters.cluster.x-k8s.io: 270 | owner: 271 | - https://github.com/orgs/giantswarm/teams/team-phoenix 272 | - https://github.com/orgs/giantswarm/teams/team-rocket 273 | provider: 274 | - aws 275 | - azure 276 | - vsphere 277 | topics: 278 | - workloadcluster 279 | drainerconfigs.core.giantswarm.io: 280 | hidden: true 281 | draughtsmanconfigs.core.giantswarm.io: 282 | hidden: true 283 | eksconfigs.bootstrap.cluster.x-k8s.io: 284 | owner: 285 | - https://github.com/orgs/giantswarm/teams/team-phoenix 286 | hidden: true 287 | eksconfigtemplates.bootstrap.cluster.x-k8s.io: 288 | owner: 289 | - https://github.com/orgs/giantswarm/teams/team-phoenix 290 | hidden: true 291 | etcdbackups.backup.giantswarm.io: 292 | hidden: true 293 | flannelconfigs.core.giantswarm.io: 294 | hidden: true 295 | g8scontrolplanes.infrastructure.giantswarm.io: 296 | owner: 297 | - https://github.com/orgs/giantswarm/teams/team-phoenix 298 | provider: 299 | - aws 300 | topics: 301 | - workloadcluster 302 | deprecation: 303 | info: This CRD will be removed once Cluster API resources are used for all AWS workload clusters. 304 | haproxyloadbalancers.infrastructure.cluster.x-k8s.io: 305 | hidden: true 306 | ingressconfigs.core.giantswarm.io: 307 | hidden: true 308 | kubeadmconfigs.bootstrap.cluster.x-k8s.io: 309 | owner: 310 | - https://github.com/orgs/giantswarm/teams/team-phoenix 311 | - https://github.com/orgs/giantswarm/teams/team-rocket 312 | hidden: true 313 | kubeadmconfigtemplates.bootstrap.cluster.x-k8s.io: 314 | owner: 315 | - https://github.com/orgs/giantswarm/teams/team-phoenix 316 | - https://github.com/orgs/giantswarm/teams/team-rocket 317 | hidden: true 318 | kubeadmcontrolplanes.controlplane.cluster.x-k8s.io: 319 | owner: 320 | - https://github.com/orgs/giantswarm/teams/team-phoenix 321 | - https://github.com/orgs/giantswarm/teams/team-rocket 322 | hidden: true 323 | kvmclusterconfigs.core.giantswarm.io: 324 | owner: 325 | - https://github.com/orgs/giantswarm/teams/team-rocket 326 | provider: 327 | - kvm 328 | topics: 329 | - workloadcluster 330 | kvmconfigs.provider.giantswarm.io: 331 | owner: 332 | - https://github.com/orgs/giantswarm/teams/team-rocket 333 | provider: 334 | - kvm 335 | topics: 336 | - workloadcluster 337 | machinedeployments.cluster.x-k8s.io: 338 | owner: 339 | - https://github.com/orgs/giantswarm/teams/team-phoenix 340 | - https://github.com/orgs/giantswarm/teams/team-rocket 341 | provider: 342 | - aws 343 | - vsphere 344 | topics: 345 | - workloadcluster 346 | machinehealthchecks.cluster.x-k8s.io: 347 | hidden: true 348 | machinepools.exp.cluster.x-k8s.io: 349 | owner: 350 | - https://github.com/orgs/giantswarm/teams/team-phoenix 351 | provider: 352 | - azure 353 | topics: 354 | - workloadcluster 355 | machines.cluster.x-k8s.io: 356 | hidden: true 357 | machinesets.cluster.x-k8s.io: 358 | hidden: true 359 | networkpools.infrastructure.giantswarm.io: 360 | owner: 361 | - https://github.com/orgs/giantswarm/teams/team-phoenix 362 | provider: 363 | - aws 364 | topics: 365 | - workloadcluster 366 | openstackclusters.infrastructure.cluster.x-k8s.io: 367 | hidden: true 368 | owner: 369 | - https://github.com/orgs/giantswarm/teams/team-rocket 370 | provider: 371 | - openstack 372 | topics: 373 | - workloadcluster 374 | openstackclustertemplates.infrastructure.cluster.x-k8s.io: 375 | hidden: true 376 | owner: 377 | - https://github.com/orgs/giantswarm/teams/team-rocket 378 | provider: 379 | - openstack 380 | topics: 381 | - workloadcluster 382 | openstackmachines.infrastructure.cluster.x-k8s.io: 383 | hidden: true 384 | owner: 385 | - https://github.com/orgs/giantswarm/teams/team-rocket 386 | provider: 387 | - openstack 388 | topics: 389 | - workloadcluster 390 | openstackmachinetemplates.infrastructure.cluster.x-k8s.io: 391 | hidden: true 392 | owner: 393 | - https://github.com/orgs/giantswarm/teams/team-rocket 394 | provider: 395 | - openstack 396 | topics: 397 | - workloadcluster 398 | organizations.security.giantswarm.io: 399 | owner: 400 | - https://github.com/orgs/giantswarm/teams/team-rainbow 401 | topics: 402 | - managementcluster 403 | releasecycles.release.giantswarm.io: 404 | hidden: true 405 | sparks.core.giantswarm.io: 406 | owner: 407 | - https://github.com/orgs/giantswarm/teams/team-phoenix 408 | provider: 409 | - azure 410 | topics: 411 | - workloadcluster 412 | storageconfigs.core.giantswarm.io: 413 | hidden: true 414 | topics: 415 | - managementcluster 416 | vsphereclusteridentities.infrastructure.cluster.x-k8s.io: 417 | hidden: true 418 | owner: 419 | - https://github.com/orgs/giantswarm/teams/team-rocket 420 | provider: 421 | - vsphere 422 | topics: 423 | - workloadcluster 424 | vsphereclusters.infrastructure.cluster.x-k8s.io: 425 | hidden: true 426 | owner: 427 | - https://github.com/orgs/giantswarm/teams/team-rocket 428 | provider: 429 | - vsphere 430 | topics: 431 | - workloadcluster 432 | vsphereclustertemplates.infrastructure.cluster.x-k8s.io: 433 | hidden: true 434 | owner: 435 | - https://github.com/orgs/giantswarm/teams/team-rocket 436 | provider: 437 | - vsphere 438 | topics: 439 | - workloadcluster 440 | vspheredeploymentzones.infrastructure.cluster.x-k8s.io: 441 | hidden: true 442 | owner: 443 | - https://github.com/orgs/giantswarm/teams/team-rocket 444 | provider: 445 | - vsphere 446 | topics: 447 | - workloadcluster 448 | vspherefailuredomains.infrastructure.cluster.x-k8s.io: 449 | hidden: true 450 | owner: 451 | - https://github.com/orgs/giantswarm/teams/team-rocket 452 | provider: 453 | - vsphere 454 | topics: 455 | - workloadcluster 456 | vspheremachines.infrastructure.cluster.x-k8s.io: 457 | hidden: true 458 | owner: 459 | - https://github.com/orgs/giantswarm/teams/team-rocket 460 | provider: 461 | - vsphere 462 | topics: 463 | - workloadcluster 464 | vspheremachinetemplates.infrastructure.cluster.x-k8s.io: 465 | hidden: true 466 | owner: 467 | - https://github.com/orgs/giantswarm/teams/team-rocket 468 | provider: 469 | - vsphere 470 | topics: 471 | - workloadcluster 472 | vspherevms.infrastructure.cluster.x-k8s.io: 473 | hidden: true 474 | owner: 475 | - https://github.com/orgs/giantswarm/teams/team-rocket 476 | provider: 477 | - vsphere 478 | topics: 479 | - workloadcluster 480 | -------------------------------------------------------------------------------- /error/error.go: -------------------------------------------------------------------------------- 1 | package error 2 | 3 | import ( 4 | "github.com/giantswarm/microerror" 5 | ) 6 | 7 | var ExecutionError = µerror.Error{ 8 | Kind: "executionError", 9 | } 10 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/giantswarm/crd-docs-generator 2 | 3 | go 1.24.0 4 | 5 | toolchain go1.24.3 6 | 7 | require ( 8 | github.com/Masterminds/sprig/v3 v3.3.0 9 | github.com/ghodss/yaml v1.0.0 10 | github.com/giantswarm/microerror v0.4.1 11 | github.com/goccy/go-yaml v1.18.0 12 | github.com/google/go-cmp v0.7.0 13 | github.com/russross/blackfriday/v2 v2.1.0 14 | github.com/spf13/cobra v1.9.1 15 | k8s.io/apiextensions-apiserver v0.33.1 16 | k8s.io/apimachinery v0.33.1 17 | ) 18 | 19 | require ( 20 | dario.cat/mergo v1.0.1 // indirect 21 | github.com/Masterminds/goutils v1.1.1 // indirect 22 | github.com/Masterminds/semver/v3 v3.3.1 // indirect 23 | github.com/fxamacker/cbor/v2 v2.8.0 // indirect 24 | github.com/go-logr/logr v1.4.2 // indirect 25 | github.com/gogo/protobuf v1.3.2 // indirect 26 | github.com/google/uuid v1.6.0 // indirect 27 | github.com/huandu/xstrings v1.5.0 // indirect 28 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 29 | github.com/json-iterator/go v1.1.12 // indirect 30 | github.com/mitchellh/copystructure v1.2.0 // indirect 31 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 32 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 33 | github.com/modern-go/reflect2 v1.0.2 // indirect 34 | github.com/shopspring/decimal v1.4.0 // indirect 35 | github.com/spf13/cast v1.7.1 // indirect 36 | github.com/spf13/pflag v1.0.6 // indirect 37 | github.com/x448/float16 v0.8.4 // indirect 38 | golang.org/x/crypto v0.37.0 // indirect 39 | golang.org/x/net v0.38.0 // indirect 40 | golang.org/x/text v0.24.0 // indirect 41 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 42 | gopkg.in/inf.v0 v0.9.1 // indirect 43 | gopkg.in/yaml.v2 v2.4.0 // indirect 44 | k8s.io/klog/v2 v2.130.1 // indirect 45 | k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e // indirect 46 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect 47 | sigs.k8s.io/randfill v1.0.0 // indirect 48 | sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect 49 | sigs.k8s.io/yaml v1.4.0 // indirect 50 | ) 51 | 52 | // Required until the other dependencies are updated to use the latest version (CVE-2025-22872) 53 | replace golang.org/x/net v0.37.0 => golang.org/x/net v0.39.0 54 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= 2 | dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= 3 | github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= 4 | github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= 5 | github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= 6 | github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= 7 | github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= 8 | github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= 9 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 10 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 12 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 14 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 15 | github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU= 16 | github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= 17 | github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= 18 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 19 | github.com/giantswarm/microerror v0.4.1 h1:WMiD7HQASoUA9lZzPlPK+erCEOJ0uT4cyo18VfCXHD0= 20 | github.com/giantswarm/microerror v0.4.1/go.mod h1:URFj0gFCmZihjya6saQCXxslBrgctXb4NsXYHB5JdrI= 21 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 22 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 23 | github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= 24 | github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= 25 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 26 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 27 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 28 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 29 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 30 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 31 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 32 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 33 | github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= 34 | github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= 35 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 36 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 37 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 38 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 39 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 40 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 41 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 42 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 43 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 44 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 45 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 46 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 47 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 48 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= 49 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 50 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 51 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 52 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 53 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 54 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 55 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 56 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 57 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 58 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 59 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 60 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 61 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 62 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 63 | github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= 64 | github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= 65 | github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= 66 | github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 67 | github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= 68 | github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= 69 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 70 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 71 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 72 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 73 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 74 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 75 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 76 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 77 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 78 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 79 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 80 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 81 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 82 | golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= 83 | golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 84 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 85 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 86 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 87 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 88 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 89 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 90 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 91 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 92 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 93 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 94 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 95 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 96 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 97 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 98 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 99 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 100 | golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= 101 | golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 102 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 103 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 104 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 105 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 106 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 107 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 108 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 109 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 110 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 111 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 112 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 113 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 114 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 115 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 116 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 117 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 118 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 119 | k8s.io/apiextensions-apiserver v0.33.1 h1:N7ccbSlRN6I2QBcXevB73PixX2dQNIW0ZRuguEE91zI= 120 | k8s.io/apiextensions-apiserver v0.33.1/go.mod h1:uNQ52z1A1Gu75QSa+pFK5bcXc4hq7lpOXbweZgi4dqA= 121 | k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4= 122 | k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= 123 | k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= 124 | k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 125 | k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e h1:KqK5c/ghOm8xkHYhlodbp6i6+r+ChV2vuAuVRdFbLro= 126 | k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 127 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= 128 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= 129 | sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= 130 | sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= 131 | sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= 132 | sigs.k8s.io/structured-merge-diff/v4 v4.7.0 h1:qPeWmscJcXP0snki5IYF79Z8xrl8ETFxgMd7wez1XkI= 133 | sigs.k8s.io/structured-merge-diff/v4 v4.7.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= 134 | sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= 135 | sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= 136 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "log" 8 | "os" 9 | "path" 10 | "path/filepath" 11 | "sort" 12 | 13 | "strings" 14 | 15 | "github.com/giantswarm/microerror" 16 | "github.com/spf13/cobra" 17 | 18 | "github.com/giantswarm/crd-docs-generator/pkg/annotations" 19 | "github.com/giantswarm/crd-docs-generator/pkg/config" 20 | "github.com/giantswarm/crd-docs-generator/pkg/crd" 21 | "github.com/giantswarm/crd-docs-generator/pkg/git" 22 | "github.com/giantswarm/crd-docs-generator/pkg/output" 23 | ) 24 | 25 | // CRDDocsGenerator represents an instance of this command line tool, it carries 26 | // the cobra command which runs the process along with configuration parameters 27 | // which come in as flags on the command line. 28 | type CRDDocsGenerator struct { 29 | // Internals. 30 | rootCommand *cobra.Command 31 | 32 | // Settings/Preferences 33 | 34 | // Path to the config file 35 | configFilePath string 36 | } 37 | 38 | const ( 39 | // Target path for our clone of the apiextensions repo. 40 | repoFolder = "/tmp/gitclone" 41 | 42 | // Default path for Markdown output (if not given in config) 43 | defaultOutputPath = "./output" 44 | ) 45 | 46 | func main() { 47 | var err error 48 | 49 | var crdDocsGenerator CRDDocsGenerator 50 | { 51 | c := &cobra.Command{ 52 | Use: "crd-docs-generator", 53 | Short: "crd-docs-generator is a command line tool for generating markdown files that document Giant Swarm's custom resources", 54 | SilenceUsage: true, 55 | RunE: func(cmd *cobra.Command, args []string) error { 56 | return generateCrdDocs(crdDocsGenerator.configFilePath) 57 | }, 58 | } 59 | 60 | c.PersistentFlags().StringVar(&crdDocsGenerator.configFilePath, "config", "./config.yaml", "Path to the configuration file.") 61 | crdDocsGenerator.rootCommand = c 62 | } 63 | 64 | if err = crdDocsGenerator.rootCommand.Execute(); err != nil { 65 | printStackTrace(err) 66 | os.Exit(1) 67 | } 68 | } 69 | 70 | // generateCrdDocs is the function called from our main CLI command. 71 | func generateCrdDocs(configFilePath string) error { 72 | configuration, err := config.Read(configFilePath) 73 | if err != nil { 74 | return microerror.Mask(err) 75 | } 76 | 77 | // Full names and versions of CRDs found, to avoid duplicates. 78 | crdNameAndVersion := make(map[string]bool) 79 | 80 | outputPath := configuration.OutputPath 81 | if outputPath == "" { 82 | outputPath = defaultOutputPath 83 | } 84 | 85 | // Loop over configured repositories 86 | defer func() { _ = os.RemoveAll(repoFolder) }() 87 | for _, sourceRepo := range configuration.SourceRepositories { 88 | // List of source YAML files containing CRD definitions. 89 | crdFiles := make(map[string]bool) 90 | 91 | log.Printf("INFO - repo %s (%s)", sourceRepo.ShortName, sourceRepo.URL) 92 | clonePath := repoFolder + "/" + sourceRepo.Organization + "/" + sourceRepo.ShortName 93 | // Clone the repositories containing CRDs 94 | log.Printf("INFO - repo %s - cloning repository", sourceRepo.ShortName) 95 | err = git.CloneRepositoryShallow( 96 | sourceRepo.Organization, 97 | sourceRepo.ShortName, 98 | sourceRepo.CommitReference, 99 | clonePath) 100 | if err != nil { 101 | return microerror.Mask(err) 102 | } 103 | 104 | // Collect CRD YAML files 105 | for _, crdPath := range sourceRepo.CRDPaths { 106 | thisCRDFolder := clonePath + "/" + crdPath 107 | err = filepath.Walk(thisCRDFolder, func(path string, info os.FileInfo, err error) error { 108 | if strings.HasSuffix(path, ".yaml") { 109 | crdFiles[path] = true 110 | } 111 | return nil 112 | }) 113 | if err != nil { 114 | return microerror.Mask(err) 115 | } 116 | } 117 | 118 | // Collect annotation info 119 | var repoAnnotations []annotations.CRDAnnotationSupport 120 | for _, annotationsPath := range sourceRepo.AnnotationsPath { 121 | thisAnnotationsFolder := clonePath + "/" + annotationsPath 122 | log.Printf("INFO - repo %s - collecting annotations in %s", sourceRepo.ShortName, thisAnnotationsFolder) 123 | a, err := annotations.Collect(thisAnnotationsFolder) 124 | if err != nil { 125 | log.Printf("ERROR - repo %s - collecting annotations in %s yielded error %#v", sourceRepo.ShortName, thisAnnotationsFolder, err) 126 | } 127 | repoAnnotations = append(repoAnnotations, a...) 128 | } 129 | 130 | crdFilesSlice := []string{} 131 | for crdFile := range crdFiles { 132 | crdFilesSlice = append(crdFilesSlice, crdFile) 133 | } 134 | 135 | sort.Strings(crdFilesSlice) 136 | for _, crdFile := range crdFilesSlice { 137 | log.Printf("INFO - repo %s - reading CRDs from file %s", sourceRepo.ShortName, crdFile) 138 | 139 | crds, err := crd.Read(crdFile) 140 | if err != nil { 141 | log.Printf("WARN - something went wrong in crd.Read for file %s: %#v", crdFile, err) 142 | } 143 | 144 | for i := range crds { 145 | // Collect versions of this CRD 146 | versions := []string{} 147 | for _, v := range crds[i].Spec.Versions { 148 | fullKey := crds[i].Name + "_" + v.Name 149 | 150 | _, exists := crdNameAndVersion[fullKey] 151 | if exists { 152 | log.Printf("WARN - repo %s - file %s provides CRD %s version %s which is already added - skipping", sourceRepo.ShortName, crdFile, crds[i].Name, v.Name) 153 | continue 154 | } 155 | crdNameAndVersion[fullKey] = true 156 | versions = append(versions, v.Name) 157 | } 158 | 159 | if len(versions) == 0 { 160 | log.Printf("WARN - repo %s - CRD %s in file %s provides no versions - skipping", sourceRepo.ShortName, crds[i].Name, crdFile) 161 | continue 162 | } 163 | log.Printf("INFO - repo %s - processing CRD %s with versions %v", sourceRepo.ShortName, crds[i].Name, versions) 164 | 165 | // Skip hidden CRDs and CRDs with missing metadata 166 | meta, ok := sourceRepo.Metadata[crds[i].Name] 167 | if !ok { 168 | log.Printf("WARN - repo %s - skipping %s as no metadata found", sourceRepo.ShortName, crds[i].Name) 169 | continue 170 | } 171 | if meta.Hidden { 172 | log.Printf("INFO - repo %s - skipping %s as hidden by configuration", sourceRepo.ShortName, crds[i].Name) 173 | continue 174 | } 175 | 176 | // Get at most one example CR for each version of this CRD 177 | exampleCRs := make(map[string]string) 178 | for _, version := range versions { 179 | found := false 180 | 181 | for _, crPath := range sourceRepo.CRPaths { 182 | // Check if the example CR in the existing format exists (e.g. observability.giantswarm.io_v1alpha1_grafanaorganization.yaml) 183 | crFilePath := fmt.Sprintf("%s/%s/%s_%s_%s.yaml", clonePath, crPath, crds[i].Spec.Group, version, crds[i].Spec.Names.Singular) 184 | if _, err := os.Stat(crFilePath); errors.Is(err, os.ErrNotExist) { 185 | if crGroupAndDomain := strings.Split(crds[i].Spec.Group, "."); len(crGroupAndDomain) >= 2 { 186 | // Check if the example CR in the kube-builder format exists (e.g. observability_v1alpha1_grafanaorganization.yaml) 187 | crFilePath = fmt.Sprintf("%s/%s/%s_%s_%s.yaml", clonePath, crPath, crGroupAndDomain[0], version, crds[i].Spec.Names.Singular) 188 | if _, err := os.Stat(crFilePath); errors.Is(err, os.ErrNotExist) { 189 | continue 190 | } 191 | } else { 192 | continue 193 | } 194 | } 195 | 196 | crFilePath = filepath.Clean(crFilePath) 197 | exampleCR, err := os.ReadFile(crFilePath) 198 | if err != nil { 199 | log.Printf("ERROR - repo %s - example CR %s could not be read: %s", sourceRepo.ShortName, crFilePath, err) 200 | } else { 201 | found = true 202 | exampleCRs[version] = strings.TrimSpace(string(exampleCR)) 203 | break 204 | } 205 | } 206 | 207 | if !found { 208 | log.Printf("WARN - repo %s - No example CR found for %s version %s", sourceRepo.ShortName, crds[i].Name, version) 209 | } 210 | } 211 | 212 | templatePath := path.Dir(configFilePath) + "/" + configuration.TemplatePath 213 | 214 | crdAnnotations := annotations.FilterForCRD(repoAnnotations, crds[i].Name, "") 215 | 216 | _, err = output.WritePage( 217 | crds[i], 218 | crdAnnotations, 219 | meta, 220 | exampleCRs, 221 | outputPath, 222 | sourceRepo.URL, 223 | sourceRepo.CommitReference, 224 | templatePath) 225 | if err != nil { 226 | log.Printf("WARN - repo %s - something went wrong in WriteCRDDocs: %#v", sourceRepo.ShortName, err) 227 | } 228 | } 229 | } 230 | } 231 | 232 | return nil 233 | } 234 | 235 | func printStackTrace(err error) { 236 | fmt.Println("\n--- Stack Trace ---") 237 | var stackedError microerror.JSONError 238 | jsonErr := json.Unmarshal([]byte(microerror.JSON(err)), &stackedError) 239 | if jsonErr != nil { 240 | fmt.Println("Error when trying to Unmarshal JSON error:") 241 | log.Printf("%#v", jsonErr) 242 | fmt.Println("\nOriginal error:") 243 | log.Printf("%#v", err) 244 | } 245 | 246 | for i, j := 0, len(stackedError.Stack)-1; i < j; i, j = i+1, j-1 { 247 | stackedError.Stack[i], stackedError.Stack[j] = stackedError.Stack[j], stackedError.Stack[i] 248 | } 249 | 250 | for _, entry := range stackedError.Stack { 251 | log.Printf("%s:%d", entry.File, entry.Line) 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "io" 6 | "os" 7 | "path/filepath" 8 | "testing" 9 | 10 | "github.com/google/go-cmp/cmp" 11 | ) 12 | 13 | var ( 14 | update = flag.Bool("update", false, "update the golden files of this test") 15 | ) 16 | 17 | func TestMain(m *testing.M) { 18 | flag.Parse() 19 | os.Exit(m.Run()) 20 | } 21 | 22 | func Test_generateCrdDocs(t *testing.T) { 23 | type args struct { 24 | configFilePath string 25 | } 26 | tests := []struct { 27 | name string 28 | args args 29 | golden string 30 | outputFile string 31 | wantErr bool 32 | }{ 33 | { 34 | name: "case1", 35 | args: args{ 36 | configFilePath: "testdata/case1/config.yaml", 37 | }, 38 | golden: "testdata/case1/output.golden", 39 | outputFile: "testdata/case1/output/examples.example.giantswarm.io.md", 40 | wantErr: false, 41 | }, 42 | } 43 | for _, tt := range tests { 44 | t.Run(tt.name, func(t *testing.T) { 45 | err := generateCrdDocs(tt.args.configFilePath) 46 | if (err != nil) != tt.wantErr { 47 | t.Errorf("generateCrdDocs() error = %v, wantErr %v", err, tt.wantErr) 48 | } 49 | 50 | content, err := os.ReadFile(tt.outputFile) 51 | if err != nil { 52 | t.Fatalf("Error loading output file %s: %s", tt.outputFile, err) 53 | } 54 | got := string(content) 55 | 56 | want := goldenValue(t, tt.golden, got, *update) 57 | if diff := cmp.Diff(want, got); diff != "" { 58 | t.Errorf("generateCrdDocs() mismatch (-want +got):\n%s", diff) 59 | } 60 | }) 61 | } 62 | } 63 | 64 | func goldenValue(t *testing.T, goldenPath string, actual string, update bool) string { 65 | t.Helper() 66 | 67 | goldenPath = filepath.Clean(goldenPath) 68 | f, _ := os.OpenFile(goldenPath, os.O_RDWR, 0600) 69 | defer func() { _ = f.Close() }() 70 | 71 | if update { 72 | _, err := f.WriteString(actual) 73 | if err != nil { 74 | t.Fatalf("Error writing to file %s: %s", goldenPath, err) 75 | } 76 | 77 | return actual 78 | } 79 | 80 | content, err := io.ReadAll(f) 81 | if err != nil { 82 | t.Fatalf("Error opening file %s: %s", goldenPath, err) 83 | } 84 | return string(content) 85 | } 86 | -------------------------------------------------------------------------------- /pkg/annotations/annotations.go: -------------------------------------------------------------------------------- 1 | package annotations 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "go/ast" 7 | "go/doc" 8 | "go/parser" 9 | "go/token" 10 | "log" 11 | "os" 12 | "path/filepath" 13 | "sort" 14 | "strings" 15 | 16 | "github.com/giantswarm/microerror" 17 | "github.com/goccy/go-yaml" 18 | ) 19 | 20 | const CRD_DOCS_GENERATOR = "CRD_DOCS_GENERATOR" 21 | 22 | type Annotation struct { 23 | Documentation string 24 | Support []AnnotationSupportRelease 25 | } 26 | 27 | type AnnotationSupportRelease struct { 28 | Release string 29 | APIVersion string 30 | CRD string 31 | } 32 | 33 | // CRDAnnotationSupport is a flattened combination of 34 | // annotation details and the CRD they are applicable to. 35 | type CRDAnnotationSupport struct { 36 | Annotation string 37 | CRDName string 38 | CRDVersion string 39 | Release string 40 | Documentation string 41 | } 42 | 43 | // Collect finds all annotations in a folder and 44 | // returns them. 45 | func Collect(startPath string) ([]CRDAnnotationSupport, error) { 46 | var annotations []CRDAnnotationSupport 47 | 48 | files, err := findFiles(startPath) 49 | if err != nil { 50 | return annotations, microerror.Mask(err) 51 | } 52 | 53 | for _, annotationFile := range files { 54 | fset := token.NewFileSet() 55 | files := []*ast.File{ 56 | mustParse(fset, annotationFile), 57 | } 58 | 59 | p, err := doc.NewFromFiles(fset, files, "github.com/giantswarm/extract-go-doc/p") 60 | if err != nil { 61 | panic(err) 62 | } 63 | 64 | for _, constant := range p.Consts { 65 | annotation, err := parseAnnotation(constant.Doc) 66 | if err != nil { 67 | log.Printf("WARN - Annotation in %s named %q does not provide compatible YAML docs", annotationFile, constant.Names[0]) 68 | continue 69 | } 70 | 71 | if annotation.Documentation != "" { 72 | for _, crdAPI := range annotation.Support { 73 | rawAnnotation := constant.Decl.Specs[0].(*ast.ValueSpec).Values[0].(*ast.BasicLit).Value 74 | annotationValue := strings.ReplaceAll(rawAnnotation, "\"", "") 75 | 76 | annotations = append(annotations, CRDAnnotationSupport{ 77 | Annotation: annotationValue, 78 | CRDName: crdAPI.CRD, 79 | CRDVersion: crdAPI.APIVersion, 80 | Release: crdAPI.Release, 81 | Documentation: annotation.Documentation, 82 | }) 83 | } 84 | } 85 | } 86 | } 87 | 88 | return Sort(annotations), nil 89 | } 90 | 91 | func parseAnnotation(rawAnnotation string) (*Annotation, error) { 92 | lines := strings.Split(rawAnnotation, "\n") 93 | 94 | crdCocsLineIndex := getCrdDocsLineIndex(lines) 95 | if crdCocsLineIndex == -1 { 96 | return nil, fmt.Errorf("no %s line found", CRD_DOCS_GENERATOR) 97 | } 98 | 99 | lines = lines[crdCocsLineIndex+1:] 100 | lines = unIndent(lines) 101 | rawAnnotation = strings.Join(lines, "\n") 102 | 103 | annotation := &Annotation{} 104 | reader := bytes.NewReader([]byte(rawAnnotation)) 105 | // Fail on unknown fields. 106 | decoder := yaml.NewDecoder(reader, yaml.DisallowUnknownField()) 107 | err := decoder.Decode(annotation) 108 | if err != nil { 109 | return nil, err 110 | } 111 | 112 | return annotation, nil 113 | } 114 | 115 | func getCrdDocsLineIndex(lines []string) int { 116 | for i, line := range lines { 117 | if strings.Contains(line, CRD_DOCS_GENERATOR) { 118 | return i 119 | } 120 | } 121 | 122 | return -1 123 | } 124 | 125 | // removes leading tabs from each line 126 | func unIndent(lines []string) []string { 127 | var result []string 128 | for _, line := range lines { 129 | result = append(result, strings.TrimPrefix(line, "\t")) 130 | } 131 | 132 | return result 133 | } 134 | 135 | func findFiles(startPath string) ([]string, error) { 136 | annotationFiles := []string{} 137 | err := filepath.Walk(startPath, func(path string, info os.FileInfo, err error) error { 138 | if strings.HasSuffix(path, ".go") { 139 | annotationFiles = append(annotationFiles, path) 140 | } 141 | return nil 142 | }) 143 | if err != nil { 144 | return annotationFiles, microerror.Mask(err) 145 | } 146 | 147 | return annotationFiles, nil 148 | } 149 | 150 | func mustParse(fset *token.FileSet, filename string) *ast.File { 151 | filename = filepath.Clean(filename) 152 | src, err := os.ReadFile(filename) 153 | if err != nil { 154 | panic(err) 155 | } 156 | f, err := parser.ParseFile(fset, filename, src, parser.ParseComments) 157 | if err != nil { 158 | panic(err) 159 | } 160 | return f 161 | } 162 | 163 | func FilterForCRD(annotations []CRDAnnotationSupport, crdName string, version string) []CRDAnnotationSupport { 164 | var result []CRDAnnotationSupport 165 | 166 | for _, annotation := range annotations { 167 | if annotation.CRDName != crdName { 168 | continue 169 | } 170 | if version != "" && annotation.CRDVersion != version { 171 | continue 172 | } 173 | result = append(result, annotation) 174 | } 175 | 176 | return result 177 | } 178 | 179 | func Sort(annotations []CRDAnnotationSupport) []CRDAnnotationSupport { 180 | sort.Slice(annotations, func(i, j int) bool { 181 | return annotations[i].Annotation < annotations[j].Annotation 182 | }) 183 | 184 | return annotations 185 | } 186 | -------------------------------------------------------------------------------- /pkg/annotations/annotations_test.go: -------------------------------------------------------------------------------- 1 | package annotations 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func Test_findFiles(t *testing.T) { 9 | type args struct { 10 | startPath string 11 | } 12 | tests := []struct { 13 | name string 14 | args args 15 | want []string 16 | wantErr bool 17 | }{ 18 | { 19 | name: "Success", 20 | args: args{startPath: "testdata"}, 21 | want: []string{"testdata/aws.go"}, 22 | wantErr: false, 23 | }, 24 | { 25 | name: "Non-existing path", 26 | args: args{startPath: "dfsgdfggh"}, 27 | want: []string{}, 28 | wantErr: false, 29 | }, 30 | } 31 | for _, tt := range tests { 32 | t.Run(tt.name, func(t *testing.T) { 33 | got, err := findFiles(tt.args.startPath) 34 | if (err != nil) != tt.wantErr { 35 | t.Errorf("findFiles() error = %#v, wantErr %#v", err, tt.wantErr) 36 | return 37 | } 38 | if !reflect.DeepEqual(got, tt.want) { 39 | t.Errorf("findFiles() = %v, want %v", got, tt.want) 40 | } 41 | }) 42 | } 43 | } 44 | 45 | func TestCollect(t *testing.T) { 46 | type args struct { 47 | startPath string 48 | } 49 | tests := []struct { 50 | name string 51 | args args 52 | want []CRDAnnotationSupport 53 | wantErr bool 54 | }{ 55 | { 56 | name: "Successful", 57 | args: args{ 58 | startPath: "testdata", 59 | }, 60 | want: []CRDAnnotationSupport{ 61 | { 62 | Annotation: "alpha.cni.aws.giantswarm.io/minimum-ip-target", 63 | CRDName: "awsclusters.infrastructure.giantswarm.io", 64 | CRDVersion: "v1alpha2", 65 | Release: "Since 14.0.0", 66 | Documentation: "This annotation allows configuration of the MINIMUM_IP_TARGET parameter for AWS CNI.", 67 | }, 68 | }, 69 | wantErr: false, 70 | }, 71 | } 72 | for _, tt := range tests { 73 | t.Run(tt.name, func(t *testing.T) { 74 | got, err := Collect(tt.args.startPath) 75 | if (err != nil) != tt.wantErr { 76 | t.Errorf("Collect() error = %#v, wantErr %#v", err, tt.wantErr) 77 | return 78 | } 79 | if !reflect.DeepEqual(got, tt.want) { 80 | t.Errorf("Collect() = %#v,\nwant %#v", got, tt.want) 81 | } 82 | }) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /pkg/annotations/testdata/aws.go: -------------------------------------------------------------------------------- 1 | // This is an example Go file 2 | // containing special doc comments we need to 3 | // associate annotations with CRDs. 4 | package aws 5 | 6 | // Comment above is possible 7 | // CRD_DOCS_GENERATOR: 8 | // 9 | // support: 10 | // - crd: awsclusters.infrastructure.giantswarm.io 11 | // apiversion: v1alpha2 12 | // release: Since 14.0.0 13 | // documentation: 14 | // This annotation allows configuration of the MINIMUM_IP_TARGET parameter for AWS CNI. 15 | const AWSCNIMinimumIPTarget = "alpha.cni.aws.giantswarm.io/minimum-ip-target" 16 | -------------------------------------------------------------------------------- /pkg/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/giantswarm/microerror" 9 | "github.com/goccy/go-yaml" 10 | ) 11 | 12 | // FromFile represent a config file content. 13 | type FromFile struct { 14 | SourceRepositories []SourceRepository `yaml:"source_repositories"` 15 | TemplatePath string `yaml:"template_path"` 16 | OutputPath string `yaml:"output_path"` 17 | } 18 | 19 | // SourceRepository has details about a 20 | // source repository to find CRDs in. 21 | type SourceRepository struct { 22 | URL string `yaml:"url"` 23 | Organization string `yaml:"organization"` 24 | ShortName string `yaml:"short_name"` 25 | Metadata map[string]CRDItem `yaml:"metadata"` 26 | CommitReference string `yaml:"commit_reference"` 27 | AnnotationsPath []string `yaml:"annotations_paths"` 28 | CRDPaths []string `yaml:"crd_paths"` 29 | CRPaths []string `yaml:"cr_paths"` 30 | } 31 | 32 | type CRDItem struct { 33 | Owners []string `yaml:"owner,omitempty"` 34 | Topics []string `yaml:"topics,omitempty"` 35 | Providers []string `yaml:"provider,omitempty"` 36 | Hidden bool `yaml:"hidden,omitempty"` 37 | Deprecation *Deprecation `yaml:"deprecation,omitempty"` 38 | } 39 | 40 | type Deprecation struct { 41 | Info string `yaml:"info,omitempty"` 42 | ReplacedBy *DeprecationReplacedBy `yaml:"replaced_by,omitempty"` 43 | } 44 | 45 | type DeprecationReplacedBy struct { 46 | FullName string `yaml:"full_name"` 47 | ShortName string `yaml:"short_name"` 48 | } 49 | 50 | // Read reads a config file and returns a struct. 51 | func Read(path string) (*FromFile, error) { 52 | f := &FromFile{} 53 | 54 | path = filepath.Clean(path) 55 | data, err := os.ReadFile(path) 56 | if err != nil { 57 | return nil, microerror.Maskf(CouldNotReadConfigFileError, "%s", err.Error()) 58 | } 59 | 60 | reader := bytes.NewReader(data) 61 | // Fail on unknown fields. 62 | decoder := yaml.NewDecoder(reader, yaml.DisallowUnknownField()) 63 | err = decoder.Decode(f) 64 | if err != nil { 65 | return nil, microerror.Maskf(CouldNotParseConfigFileError, "%s", err.Error()) 66 | } 67 | 68 | return f, nil 69 | } 70 | -------------------------------------------------------------------------------- /pkg/config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestRead(t *testing.T) { 9 | type args struct { 10 | path string 11 | } 12 | tests := []struct { 13 | name string 14 | args args 15 | want *FromFile 16 | wantErr bool 17 | }{ 18 | { 19 | name: "test 1 - read valid config file", 20 | args: args{ 21 | path: "testdata/config1.yaml", 22 | }, 23 | want: &FromFile{ 24 | SourceRepositories: []SourceRepository{ 25 | { 26 | URL: "https://github.com/giantswarm/apiextensions", 27 | Organization: "giantswarm", 28 | ShortName: "apiextensions", 29 | AnnotationsPath: []string{"pkg/annotations"}, 30 | CRDPaths: []string{"config/crd", "helm"}, 31 | CRPaths: []string{"docs/cr"}, 32 | Metadata: map[string]CRDItem{ 33 | "crd.with.full.info": { 34 | Owners: []string{"owner"}, 35 | Topics: []string{"apps"}, 36 | Providers: []string{"aws", "azure"}, 37 | Hidden: false, 38 | }, 39 | "unpublished.crd": { 40 | Hidden: true, 41 | }, 42 | "only.defaults": { 43 | Hidden: false, 44 | }, 45 | "deprecated.crd": { 46 | Hidden: false, 47 | Deprecation: &Deprecation{ 48 | ReplacedBy: &DeprecationReplacedBy{ 49 | FullName: "new.full.crd.name", 50 | ShortName: "New", 51 | }, 52 | }, 53 | }, 54 | "simply.deprecated.crd": { 55 | Hidden: false, 56 | Deprecation: &Deprecation{ 57 | Info: "This CRD is deprecated", 58 | }, 59 | }, 60 | }, 61 | CommitReference: "v3.39.0", 62 | }, 63 | }, 64 | OutputPath: "output", 65 | TemplatePath: "my/file", 66 | }, 67 | wantErr: false, 68 | }, 69 | { 70 | name: "test 2 - file does not exist", 71 | args: args{ 72 | path: "testdata/foo", 73 | }, 74 | want: nil, 75 | wantErr: true, 76 | }, 77 | { 78 | name: "test 3 - invalid file", 79 | args: args{ 80 | path: "testdata/config2.yaml", 81 | }, 82 | want: nil, 83 | wantErr: true, 84 | }, 85 | } 86 | for _, tt := range tests { 87 | t.Run(tt.name, func(t *testing.T) { 88 | got, err := Read(tt.args.path) 89 | if (err != nil) != tt.wantErr { 90 | t.Errorf("Read() error = %v, wantErr %v", err, tt.wantErr) 91 | return 92 | } 93 | if !reflect.DeepEqual(got, tt.want) { 94 | t.Errorf("Read() = %v, want %v", got, tt.want) 95 | } 96 | }) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /pkg/config/error.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "github.com/giantswarm/microerror" 4 | 5 | var CouldNotReadConfigFileError = µerror.Error{ 6 | Kind: "CouldNotReadConfigFileError", 7 | Desc: "The configuration file could not be read.", 8 | } 9 | 10 | // IsCouldNotReadConfigFile asserts CouldNotReadConfigFileError 11 | func IsCouldNotReadConfigFile(e error) bool { 12 | return microerror.Cause(e) == CouldNotReadConfigFileError 13 | } 14 | 15 | var CouldNotParseConfigFileError = µerror.Error{ 16 | Kind: "CouldNotParseConfigFileError", 17 | Desc: "The configuration file could not be parsed.", 18 | } 19 | 20 | // IsCouldNotParseConfigFile asserts CouldNotParseConfigFileError 21 | func IsCouldNotParseConfigFile(e error) bool { 22 | return microerror.Cause(e) == CouldNotParseConfigFileError 23 | } 24 | -------------------------------------------------------------------------------- /pkg/config/testdata/config1.yaml: -------------------------------------------------------------------------------- 1 | template_path: my/file 2 | 3 | output_path: output 4 | 5 | source_repositories: 6 | - url: https://github.com/giantswarm/apiextensions 7 | organization: giantswarm 8 | short_name: apiextensions 9 | commit_reference: v3.39.0 10 | annotations_paths: 11 | - pkg/annotations 12 | crd_paths: 13 | - config/crd 14 | - helm 15 | cr_paths: 16 | - docs/cr 17 | metadata: 18 | crd.with.full.info: 19 | owner: 20 | - owner 21 | topics: 22 | - apps 23 | provider: 24 | - aws 25 | - azure 26 | hidden: false 27 | unpublished.crd: 28 | hidden: true 29 | only.defaults: 30 | deprecated.crd: 31 | deprecation: 32 | replaced_by: 33 | full_name: new.full.crd.name 34 | short_name: New 35 | simply.deprecated.crd: 36 | deprecation: 37 | info: This CRD is deprecated 38 | -------------------------------------------------------------------------------- /pkg/config/testdata/config2.yaml: -------------------------------------------------------------------------------- 1 | invalid: true 2 | makes: nosense 3 | -------------------------------------------------------------------------------- /pkg/crd/crd.go: -------------------------------------------------------------------------------- 1 | package crd 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "strings" 7 | 8 | "github.com/ghodss/yaml" 9 | "github.com/giantswarm/microerror" 10 | apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 11 | ) 12 | 13 | // Read reads a CRD YAML file and returns the Custom Resource Definition objects it represents. 14 | func Read(filePath string) ([]apiextensionsv1.CustomResourceDefinition, error) { 15 | filePath = filepath.Clean(filePath) 16 | yamlBytes, err := os.ReadFile(filePath) 17 | if err != nil { 18 | return nil, microerror.Maskf(CouldNotReadCRDFileError, "%s", err.Error()) 19 | } 20 | 21 | // Split by "---" 22 | parts := strings.Split(string(yamlBytes), "\n---\n") 23 | crds := []apiextensionsv1.CustomResourceDefinition{} 24 | 25 | for _, crdYAMLString := range parts { 26 | crdYAMLBytes := []byte(crdYAMLString) 27 | crd := apiextensionsv1.CustomResourceDefinition{} 28 | 29 | err = yaml.Unmarshal(crdYAMLBytes, &crd) 30 | if err != nil { 31 | return nil, microerror.Maskf(CouldNotParseCRDFileError, "%s", err.Error()) 32 | } 33 | 34 | // If we had empty parts parsed, let's skip them. 35 | if crd.Name == "" { 36 | continue 37 | } 38 | 39 | crds = append(crds, crd) 40 | } 41 | 42 | return crds, nil 43 | } 44 | -------------------------------------------------------------------------------- /pkg/crd/crd_test.go: -------------------------------------------------------------------------------- 1 | package crd 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/google/go-cmp/cmp" 7 | apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | ) 10 | 11 | func TestRead(t *testing.T) { 12 | type args struct { 13 | filePath string 14 | } 15 | tests := []struct { 16 | name string 17 | args args 18 | want []apiextensionsv1.CustomResourceDefinition 19 | wantErr bool 20 | }{ 21 | { 22 | name: "file with one CRD with one version and some schema", 23 | args: args{ 24 | filePath: "testdata/awsclusterconfig.yaml", 25 | }, 26 | want: []apiextensionsv1.CustomResourceDefinition{ 27 | { 28 | TypeMeta: metav1.TypeMeta{ 29 | Kind: "CustomResourceDefinition", 30 | APIVersion: "apiextensions.k8s.io/v1", 31 | }, 32 | ObjectMeta: metav1.ObjectMeta{ 33 | Name: "awsclusterconfigs.core.giantswarm.io", 34 | Annotations: map[string]string{ 35 | "controller-gen.kubebuilder.io/version": "v0.2.4", 36 | }, 37 | }, 38 | Spec: apiextensionsv1.CustomResourceDefinitionSpec{ 39 | Group: "core.giantswarm.io", 40 | Names: apiextensionsv1.CustomResourceDefinitionNames{ 41 | Plural: "awsclusterconfigs", 42 | Singular: "awsclusterconfig", 43 | Kind: "AWSClusterConfig", 44 | ListKind: "AWSClusterConfigList", 45 | Categories: []string{"aws", "giantswarm"}, 46 | }, 47 | Scope: "Namespaced", 48 | Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ 49 | { 50 | Name: "v1alpha1", 51 | Served: true, 52 | Storage: true, 53 | Schema: &apiextensionsv1.CustomResourceValidation{ 54 | OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ 55 | Description: "AWSClusterConfig used to represent workload cluster configuration in earlier releases. Deprecated.", 56 | Type: "object", 57 | Required: []string{ 58 | "spec", 59 | }, 60 | Properties: map[string]apiextensionsv1.JSONSchemaProps{ 61 | "apiVersion": { 62 | Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", 63 | Type: "string", 64 | }, 65 | "kind": { 66 | Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", 67 | Type: "string", 68 | }, 69 | "spec": { 70 | Type: "object", 71 | Required: []string{"someString"}, 72 | Properties: map[string]apiextensionsv1.JSONSchemaProps{ 73 | "someString": { 74 | Type: "string", 75 | }, 76 | }, 77 | }, 78 | }, 79 | }, 80 | }, 81 | }, 82 | }, 83 | }, 84 | }, 85 | }, 86 | wantErr: false, 87 | }, 88 | } 89 | for _, tt := range tests { 90 | t.Run(tt.name, func(t *testing.T) { 91 | got, err := Read(tt.args.filePath) 92 | if (err != nil) != tt.wantErr { 93 | t.Errorf("Read() error = %v, wantErr %v", err, tt.wantErr) 94 | return 95 | } 96 | if diff := cmp.Diff(tt.want, got); diff != "" { 97 | t.Errorf("Read() mismatch (-want +got):\n%s", diff) 98 | } 99 | }) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /pkg/crd/error.go: -------------------------------------------------------------------------------- 1 | package crd 2 | 3 | import "github.com/giantswarm/microerror" 4 | 5 | var CouldNotReadCRDFileError = µerror.Error{ 6 | Kind: "CouldNotReadCRDFileError", 7 | Desc: "The CRD file could not be read.", 8 | } 9 | 10 | // IsCouldNotReadCRDFile asserts CouldNotReadCRDFileError 11 | func IsCouldNotReadCRDFile(e error) bool { 12 | return microerror.Cause(e) == CouldNotReadCRDFileError 13 | } 14 | 15 | var CouldNotParseCRDFileError = µerror.Error{ 16 | Kind: "CouldNotParseCRDFileError", 17 | Desc: "The CRD file could not be parsed.", 18 | } 19 | 20 | // IsCouldNotParseCRDFile asserts CouldNotParseCRDFileError 21 | func IsCouldNotParseCRDFile(e error) bool { 22 | return microerror.Cause(e) == CouldNotParseCRDFileError 23 | } 24 | -------------------------------------------------------------------------------- /pkg/crd/testdata/awsclusterconfig.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.2.4 7 | name: awsclusterconfigs.core.giantswarm.io 8 | spec: 9 | group: core.giantswarm.io 10 | names: 11 | categories: 12 | - aws 13 | - giantswarm 14 | kind: AWSClusterConfig 15 | listKind: AWSClusterConfigList 16 | plural: awsclusterconfigs 17 | singular: awsclusterconfig 18 | scope: Namespaced 19 | versions: 20 | - name: v1alpha1 21 | schema: 22 | openAPIV3Schema: 23 | description: AWSClusterConfig used to represent workload cluster configuration in earlier releases. Deprecated. 24 | type: object 25 | properties: 26 | apiVersion: 27 | type: string 28 | description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 29 | kind: 30 | type: string 31 | description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 32 | spec: 33 | type: object 34 | properties: 35 | someString: 36 | type: string 37 | required: 38 | - someString 39 | required: 40 | - spec 41 | served: true 42 | storage: true 43 | -------------------------------------------------------------------------------- /pkg/git/git.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | 7 | "github.com/giantswarm/microerror" 8 | 9 | errorpkg "github.com/giantswarm/crd-docs-generator/error" 10 | ) 11 | 12 | // CloneRepositoryShallow will clone repository in a given directory. 13 | func CloneRepositoryShallow(user string, repo string, tag string, destDir string) error { 14 | { 15 | cmd := exec.Command("git", "clone", "-b", tag, "--depth", "1", fmt.Sprintf("https://github.com/%s/%s.git", user, repo), destDir) // nolint: gosec 16 | err := cmd.Run() 17 | if err != nil { 18 | return microerror.Maskf(errorpkg.ExecutionError, "Could not `git clone` source repository.\nTried to execute: %s\n%s", cmd.String(), err.Error()) 19 | } 20 | } 21 | 22 | return nil 23 | } 24 | -------------------------------------------------------------------------------- /pkg/jsonschema/jsonschema.go: -------------------------------------------------------------------------------- 1 | package jsonschema 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | 7 | apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 8 | ) 9 | 10 | const arrayItemIndicator = "[*]" 11 | 12 | // Property is a simplistic, flattened representation of a property 13 | // in a JSON Schema, without the recursion and containing only the elements 14 | // we intend to expose in our output. 15 | type Property struct { 16 | // The depth of the item in the JSONPath hierarchy 17 | Depth int8 18 | // Path is the full JSONpath path of the attribute, e. g. ".spec.version". 19 | Path string 20 | // Name is the attribute name. 21 | Name string 22 | // Type is the textual representaiton of the type ("object", "array", "number", "string", "boolean"). 23 | Type string 24 | // Description is a user-friendly description of the attribute. 25 | Description string 26 | // Required specifies whether the property is required. 27 | Required bool 28 | } 29 | 30 | // Flatten recurses over all properties of a JSON Schema 31 | // and returns a flat slice of the elements we need for our output. 32 | func Flatten(schema apiextensionsv1.JSONSchemaProps, properties []Property, depth int8, pathPrefix string) []Property { 33 | // Capture names of required properties. 34 | requiredProps := make(map[string]bool) 35 | for _, p := range schema.Required { 36 | requiredProps[p] = true 37 | } 38 | 39 | // Collect reduced property info. 40 | for propname, schemaProps := range schema.Properties { 41 | path := fmt.Sprintf("%s.%s", pathPrefix, propname) 42 | 43 | required := false 44 | if _, ok := requiredProps[propname]; ok { 45 | required = true 46 | } 47 | 48 | property := Property{ 49 | Depth: depth, 50 | Name: propname, 51 | Path: path, 52 | Description: schemaProps.Description, 53 | Type: schemaProps.Type, 54 | Required: required, 55 | } 56 | 57 | properties = append(properties, property) 58 | 59 | if len(schemaProps.Properties) > 0 { 60 | properties = Flatten(schemaProps, properties, depth+1, path) 61 | } 62 | 63 | if schemaProps.Type == "array" && schemaProps.Items != nil { 64 | // Add description of array member type 65 | property := Property{ 66 | Depth: depth + 1, 67 | Name: propname + arrayItemIndicator, 68 | Path: path + arrayItemIndicator, 69 | Description: schemaProps.Items.Schema.Description, 70 | Type: schemaProps.Items.Schema.Type, 71 | } 72 | properties = append(properties, property) 73 | 74 | // Collect sub items properties 75 | properties = Flatten(*schemaProps.Items.Schema, properties, depth+2, path+arrayItemIndicator) 76 | } 77 | } 78 | 79 | // Sort properties by path. 80 | sort.Slice(properties, func(i, j int) bool { 81 | return properties[i].Path < properties[j].Path 82 | }) 83 | 84 | return properties 85 | } 86 | -------------------------------------------------------------------------------- /pkg/jsonschema/jsonschema_test.go: -------------------------------------------------------------------------------- 1 | package jsonschema 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 8 | ) 9 | 10 | func TestFlatten(t *testing.T) { 11 | type args struct { 12 | schema apiextensionsv1.JSONSchemaProps 13 | properties []Property 14 | depth int8 15 | pathPrefix string 16 | } 17 | tests := []struct { 18 | name string 19 | args args 20 | want []Property 21 | }{ 22 | { 23 | name: "Nested schema", 24 | args: args{ 25 | schema: apiextensionsv1.JSONSchemaProps{ 26 | ID: "root", 27 | Description: "top description", 28 | Type: "object", 29 | Required: []string{"required_string"}, 30 | Properties: map[string]apiextensionsv1.JSONSchemaProps{ 31 | "required_string": { 32 | ID: "the_id", 33 | Description: "A required string property", 34 | Type: "string", 35 | }, 36 | "optional_array": { 37 | Type: "array", 38 | Description: "An optional array property", 39 | Items: &apiextensionsv1.JSONSchemaPropsOrArray{ 40 | Schema: &apiextensionsv1.JSONSchemaProps{ 41 | Description: "Array item", 42 | Type: "string", 43 | }, 44 | JSONSchemas: []apiextensionsv1.JSONSchemaProps{}, 45 | }, 46 | }, 47 | "optional_object": { 48 | Type: "object", 49 | Properties: map[string]apiextensionsv1.JSONSchemaProps{ 50 | "nested_number": { 51 | Type: "number", 52 | }, 53 | }, 54 | }, 55 | }, 56 | }, 57 | properties: []Property{}, 58 | depth: 0, 59 | pathPrefix: "", 60 | }, 61 | want: []Property{ 62 | { 63 | Depth: 0, 64 | Path: ".optional_array", 65 | Name: "optional_array", 66 | Type: "array", 67 | Description: "An optional array property", 68 | Required: false, 69 | }, 70 | { 71 | Depth: 1, 72 | Path: ".optional_array[*]", 73 | Name: "optional_array[*]", 74 | Type: "string", 75 | Description: "Array item", 76 | Required: false, 77 | }, 78 | { 79 | Depth: 0, 80 | Path: ".optional_object", 81 | Name: "optional_object", 82 | Type: "object", 83 | Description: "", 84 | Required: false, 85 | }, 86 | { 87 | Depth: 1, 88 | Path: ".optional_object.nested_number", 89 | Name: "nested_number", 90 | Type: "number", 91 | Description: "", 92 | Required: false, 93 | }, 94 | { 95 | Depth: 0, 96 | Path: ".required_string", 97 | Name: "required_string", 98 | Type: "string", 99 | Description: "A required string property", 100 | Required: true, 101 | }, 102 | }, 103 | }, 104 | } 105 | for _, tt := range tests { 106 | t.Run(tt.name, func(t *testing.T) { 107 | if got := Flatten(tt.args.schema, tt.args.properties, tt.args.depth, tt.args.pathPrefix); !reflect.DeepEqual(got, tt.want) { 108 | t.Errorf("Flatten() = %v, want %v", got, tt.want) 109 | } 110 | }) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /pkg/output/error.go: -------------------------------------------------------------------------------- 1 | package output 2 | 3 | import "github.com/giantswarm/microerror" 4 | 5 | var cannotOpenTemplate = µerror.Error{ 6 | Kind: "cannotOpenTemplate", 7 | } 8 | -------------------------------------------------------------------------------- /pkg/output/output.go: -------------------------------------------------------------------------------- 1 | package output 2 | 3 | import ( 4 | "fmt" 5 | "html/template" 6 | "os" 7 | "path/filepath" 8 | "sort" 9 | "strings" 10 | 11 | "github.com/Masterminds/sprig/v3" 12 | "github.com/giantswarm/microerror" 13 | blackfriday "github.com/russross/blackfriday/v2" 14 | apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 15 | 16 | "github.com/giantswarm/crd-docs-generator/pkg/annotations" 17 | "github.com/giantswarm/crd-docs-generator/pkg/config" 18 | "github.com/giantswarm/crd-docs-generator/pkg/jsonschema" 19 | ) 20 | 21 | // PageData is all the data we pass to the HTML template for the CRD detail page. 22 | type PageData struct { 23 | Description string 24 | Metadata config.CRDItem 25 | Group string 26 | NamePlural string 27 | NameSingular string 28 | Scope string 29 | SourceRepository string 30 | SourceRepositoryRef string 31 | Title string 32 | Weight int 33 | // Version names. 34 | Versions []string 35 | // Schema per version. 36 | VersionSchemas map[string]SchemaVersion 37 | } 38 | 39 | // SchemaVersion is the schema information for a specific CRD version 40 | // we want to expose to our template. 41 | type SchemaVersion struct { 42 | Version string 43 | Properties []jsonschema.Property 44 | // YAML string showing an example CR. 45 | ExampleCR string 46 | Annotations []annotations.CRDAnnotationSupport 47 | } 48 | 49 | // WritePage creates a CRD schema documentation Markdown page. 50 | func WritePage(crd apiextensionsv1.CustomResourceDefinition, 51 | crdAnnotations []annotations.CRDAnnotationSupport, 52 | md config.CRDItem, 53 | examplesCRs map[string]string, 54 | outputFolder, 55 | repoURL, 56 | repoRef, 57 | templatePath string) (string, error) { 58 | 59 | templatePath = filepath.Clean(templatePath) 60 | templateCode, err := os.ReadFile(templatePath) 61 | if err != nil { 62 | return "", microerror.Maskf(cannotOpenTemplate, "Could not read template file %s: %s", templatePath, err) 63 | } 64 | 65 | // Add custom functions support for our template. 66 | funcMap := sprig.FuncMap() 67 | // Treat given test as Markdown and convert to HTML. 68 | funcMap["markdown"] = toMarkdown 69 | // Join strings by separator 70 | funcMap["join"] = strings.Join 71 | // Return raw string 72 | funcMap["raw"] = rawString 73 | 74 | // Read our output template. 75 | tpl := template.Must(template.New("schemapage").Funcs(funcMap).Parse(string(templateCode))) 76 | 77 | // Collect values to pass to our output template. 78 | data := PageData{ 79 | Group: crd.Spec.Group, 80 | Metadata: md, 81 | NamePlural: crd.Spec.Names.Plural, 82 | NameSingular: crd.Spec.Names.Singular, 83 | Scope: string(crd.Spec.Scope), 84 | SourceRepository: repoURL, 85 | SourceRepositoryRef: repoRef, 86 | Title: crd.Spec.Names.Kind, 87 | Weight: 100, 88 | VersionSchemas: make(map[string]SchemaVersion), 89 | } 90 | 91 | // Iterate schema versions 92 | for _, version := range crd.Spec.Versions { 93 | if !version.Served && !version.Storage { 94 | // Neither stored nor served means that this version 95 | // can be skipped. 96 | continue 97 | } 98 | 99 | // Get the first non-empty top level description and use it as the 100 | // CRD description. 101 | if data.Description == "" && version.Schema != nil { 102 | data.Description = version.Schema.OpenAPIV3Schema.Description 103 | } 104 | 105 | var properties []jsonschema.Property 106 | 107 | if version.Schema != nil && version.Schema.OpenAPIV3Schema != nil { 108 | properties = jsonschema.Flatten(*version.Schema.OpenAPIV3Schema, properties, 0, "") 109 | } 110 | 111 | data.VersionSchemas[version.Name] = SchemaVersion{ 112 | Version: version.Name, 113 | Properties: properties, 114 | Annotations: annotations.FilterForCRD(crdAnnotations, crd.Name, version.Name), 115 | } 116 | 117 | data.Versions = append(data.Versions, version.Name) 118 | } 119 | 120 | sort.Strings(data.Versions) 121 | 122 | // Add example CRs 123 | for _, version := range data.Versions { 124 | exampleCR, ok := examplesCRs[version] 125 | if ok { 126 | outputSchema := data.VersionSchemas[version] 127 | outputSchema.ExampleCR = exampleCR + "\n" 128 | data.VersionSchemas[version] = outputSchema 129 | } 130 | } 131 | 132 | // Name output file after full CRD name. 133 | outputFile := outputFolder + "/" + crd.Spec.Names.Plural + "." + crd.Spec.Group + ".md" 134 | 135 | if _, err := os.Stat(outputFolder); os.IsNotExist(err) { 136 | err := os.MkdirAll(outputFolder, 0750) 137 | if err != nil { 138 | return "", microerror.Mask(err) 139 | } 140 | } 141 | 142 | outputFile = filepath.Clean(outputFile) 143 | handler, err := os.Create(outputFile) 144 | if err != nil { 145 | return "", microerror.Mask(err) 146 | } 147 | 148 | err = tpl.Execute(handler, data) 149 | if err != nil { 150 | 151 | // TODO: return error 152 | // return microerror.Mask(err) 153 | 154 | fmt.Printf("%s: %s\n", outputFile, err) 155 | } 156 | 157 | return outputFile, nil 158 | } 159 | 160 | func toMarkdown(input string) template.HTML { 161 | inputBytes := []byte(input) 162 | // To mitigate gosec "this method will not auto-escape HTML. Verify data is well formed" 163 | // #nosec G203 164 | return template.HTML(blackfriday.Run(inputBytes)) 165 | } 166 | 167 | func rawString(input string) template.HTML { 168 | // To mitigate gosec "this method will not auto-escape HTML. Verify data is well formed" 169 | // #nosec G203 170 | return template.HTML(input) 171 | } 172 | -------------------------------------------------------------------------------- /pkg/output/output_test.go: -------------------------------------------------------------------------------- 1 | package output 2 | 3 | import ( 4 | "flag" 5 | "io" 6 | "os" 7 | "testing" 8 | 9 | "github.com/giantswarm/microerror" 10 | "github.com/google/go-cmp/cmp" 11 | apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 12 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 | 14 | "github.com/giantswarm/crd-docs-generator/pkg/annotations" 15 | "github.com/giantswarm/crd-docs-generator/pkg/config" 16 | ) 17 | 18 | var ( 19 | update = flag.Bool("update", false, "update the golden files of this test") 20 | ) 21 | 22 | func TestMain(m *testing.M) { 23 | flag.Parse() 24 | os.Exit(m.Run()) 25 | } 26 | 27 | func TestWritePage(t *testing.T) { 28 | type args struct { 29 | crd apiextensionsv1.CustomResourceDefinition 30 | annotations []annotations.CRDAnnotationSupport 31 | md config.CRDItem 32 | examples map[string]string 33 | repoURL string 34 | repoRef string 35 | templatePath string 36 | } 37 | tests := []struct { 38 | name string 39 | args args 40 | wantErr error 41 | golden string 42 | }{ 43 | { 44 | name: "Test 01", 45 | args: args{ 46 | crd: apiextensionsv1.CustomResourceDefinition{ 47 | TypeMeta: metav1.TypeMeta{ 48 | Kind: "CustomResourceDefinition", 49 | APIVersion: "apiextensions.k8s.io/v1", 50 | }, 51 | ObjectMeta: metav1.ObjectMeta{ 52 | Name: "demos.demo.giantswarm.io", 53 | }, 54 | Spec: apiextensionsv1.CustomResourceDefinitionSpec{ 55 | Group: "demo.giantswarm.io", 56 | Names: apiextensionsv1.CustomResourceDefinitionNames{ 57 | Plural: "demos", 58 | Singular: "demo", 59 | ShortNames: []string{"dmo"}, 60 | Kind: "Demo", 61 | ListKind: "DemoList", 62 | Categories: []string{"first", "second"}, 63 | }, 64 | Scope: "Namespaced", 65 | Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ 66 | { 67 | Name: "v1alpha2", 68 | Served: true, 69 | Storage: true, 70 | Schema: &apiextensionsv1.CustomResourceValidation{ 71 | OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ 72 | Description: "Main description of v1alpha2", 73 | Type: "object", 74 | Format: "", 75 | Title: "Object title", 76 | Required: []string{"first_property"}, 77 | Properties: map[string]apiextensionsv1.JSONSchemaProps{ 78 | "first_property": { 79 | Description: "First property", 80 | Type: "string", 81 | Format: "", 82 | Title: "The title", 83 | }, 84 | }, 85 | }, 86 | }, 87 | }, 88 | { 89 | Name: "v1alpha1", 90 | Served: true, 91 | Storage: true, 92 | Schema: &apiextensionsv1.CustomResourceValidation{ 93 | OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ 94 | Description: "Main description of v1alpha1", 95 | Type: "object", 96 | Format: "", 97 | Title: "Object title", 98 | Required: []string{"first_property"}, 99 | Properties: map[string]apiextensionsv1.JSONSchemaProps{ 100 | "first_property": { 101 | Description: "First property", 102 | Type: "string", 103 | Format: "", 104 | Title: "The title", 105 | }, 106 | }, 107 | }, 108 | }, 109 | }, 110 | }, 111 | }, 112 | }, 113 | annotations: []annotations.CRDAnnotationSupport{ 114 | { 115 | Annotation: "alpha.giantswarm.io/foo", 116 | CRDName: "demos.demo.giantswarm.io", 117 | CRDVersion: "v1alpha2", 118 | Release: "Since v16.0.0", 119 | Documentation: "Here is some annotation documentation.", 120 | }, 121 | }, 122 | md: config.CRDItem{ 123 | Owners: []string{"first-owner", "second-owner"}, 124 | Topics: []string{"first-topic", "second-topic"}, 125 | Providers: []string{"aws", "azure"}, 126 | Deprecation: &config.Deprecation{ 127 | Info: "This is some deprecation info", 128 | ReplacedBy: &config.DeprecationReplacedBy{ 129 | FullName: "another.demo.giantswarm.io", 130 | ShortName: "Another", 131 | }, 132 | }, 133 | }, 134 | examples: map[string]string{ 135 | "v1alpha1": "This is an example CR", 136 | }, 137 | repoURL: "https://github.com/giantswarm/my-repo", 138 | repoRef: "main", 139 | templatePath: "testdata/crd.template", 140 | }, 141 | golden: "test_01", 142 | wantErr: nil, 143 | }, 144 | } 145 | for _, tt := range tests { 146 | t.Run(tt.name, func(t *testing.T) { 147 | tempDir, err := os.MkdirTemp("", "TestWritePage") 148 | if err != nil { 149 | t.Fatalf("Could not create temp dir: %s", err) 150 | } 151 | defer func() { _ = os.RemoveAll(tempDir) }() 152 | 153 | resultPath, err := WritePage(tt.args.crd, tt.args.annotations, tt.args.md, tt.args.examples, tempDir, tt.args.repoURL, tt.args.repoRef, tt.args.templatePath) 154 | if err != tt.wantErr { 155 | t.Errorf("WritePage() error = %v, wantErr %v", err, tt.wantErr) 156 | t.Logf("%s", microerror.Pretty(err, true)) 157 | } 158 | 159 | gotBytes, err := os.ReadFile(resultPath) //nolint:gosec 160 | if err != nil { 161 | t.Errorf("Could not open result file %s: %s", resultPath, err) 162 | } 163 | got := string(gotBytes) 164 | want := goldenValue(t, tt.golden, got, *update) 165 | 166 | if diff := cmp.Diff(want, got); diff != "" { 167 | t.Errorf("WritePage() mismatch (-want +got):\n%s", diff) 168 | } 169 | }) 170 | } 171 | } 172 | 173 | func goldenValue(t *testing.T, goldenFile string, actual string, update bool) string { 174 | t.Helper() 175 | goldenPath := "testdata/" + goldenFile + ".golden" 176 | 177 | f, err := os.OpenFile(goldenPath, os.O_RDWR, 0644) //nolint:gosec 178 | if err != nil { 179 | t.Fatalf("Error opening file %s: %s", goldenPath, err) 180 | } 181 | defer func() { _ = f.Close() }() 182 | 183 | if update { 184 | _, err := f.WriteString(actual) 185 | if err != nil { 186 | t.Fatalf("Error writing to file %s: %s", goldenPath, err) 187 | } 188 | 189 | return actual 190 | } 191 | 192 | content, err := io.ReadAll(f) 193 | if err != nil { 194 | t.Fatalf("Error reading content of file %s: %s", goldenPath, err) 195 | } 196 | return string(content) 197 | } 198 | -------------------------------------------------------------------------------- /pkg/output/testdata/crd.template: -------------------------------------------------------------------------------- 1 | --- 2 | title: {{ .Title }} CRD schema reference (group {{ .Group }}) 3 | linkTitle: {{ .Title }} 4 | description: | 5 | {{- if .Description }} 6 | {{ .Description | indent 2 }} 7 | {{- else }} 8 | Custom resource definition (CRD) schema reference page for the {{ .Title }} resource ({{ .NamePlural }}.{{ .Group }}), as part of the Giant Swarm Management API documentation. 9 | {{- end }} 10 | weight: {{ .Weight }} 11 | crd: 12 | name_camelcase: {{ .Title }} 13 | name_plural: {{ .NamePlural }} 14 | name_singular: {{ .NameSingular }} 15 | group: {{ .Group }} 16 | technical_name: {{ .NamePlural }}.{{ .Group }} 17 | scope: {{ .Scope }} 18 | source_repository: {{ .SourceRepository }} 19 | source_repository_ref: {{ .SourceRepositoryRef }} 20 | versions: 21 | {{- range .Versions }} 22 | - {{ . -}} 23 | {{- end }} 24 | topics: 25 | {{- with .Metadata.Topics }} 26 | {{- range . }} 27 | - {{ . -}} 28 | {{- end }} 29 | {{- end }} 30 | {{- with .Metadata.Providers }} 31 | providers: 32 | {{- range . }} 33 | - {{ . -}} 34 | {{- end }} 35 | {{- end }} 36 | {{- with .Metadata.Deprecation }} 37 | deprecation: 38 | {{- with .Info }} 39 | info: {{ . }} 40 | {{- end }} 41 | {{- with .ReplacedBy }} 42 | replaced_by: 43 | full_name: {{ .FullName }} 44 | short_name: {{ .ShortName }} 45 | {{- end }} 46 | {{- end }} 47 | layout: crd 48 | owner: 49 | {{- range .Metadata.Owners }} 50 | - {{ . -}} 51 | {{- end }} 52 | aliases: 53 | - /reference/cp-k8s-api/{{ .NamePlural }}.{{ .Group }}/ 54 | technical_name: {{ .NamePlural }}.{{ .Group }} 55 | source_repository: {{ .SourceRepository }} 56 | source_repository_ref: {{ .SourceRepositoryRef }} 57 | --- 58 | 59 | # {{ .Title }} 60 | 61 | {{ with .Description }} 62 |

{{ . }}

63 | {{ end -}} 64 | 65 |
66 |
Full name:
67 |
{{ .NamePlural }}.{{ .Group }}
68 |
Group:
69 |
{{ .Group }}
70 |
Singular name:
71 |
{{ .NameSingular }}
72 |
Plural name:
73 |
{{ .NamePlural }}
74 |
Scope:
75 |
{{ .Scope }}
76 |
Versions:
77 | {{ range .Versions -}} 78 |
{{.}}
79 | {{ end -}} 80 |
81 | 82 | {{ if .VersionSchemas }} 83 | {{ $versionSchemas := .VersionSchemas }} 84 | {{ range .Versions -}} 85 | {{ $versionName := . -}} 86 | {{ $versionSchema := (index $versionSchemas $versionName) -}} 87 |
88 |

Version {{$versionName}}

89 | 90 | {{with $versionSchema.ExampleCR}} 91 |

Example CR

92 | 93 | ```yaml 94 | {{ .|raw -}} 95 | ``` 96 | {{end}} 97 | 98 |

Properties

99 | 100 | {{ range $versionSchema.Properties }} 101 |
102 |
103 |

{{.Path}}

104 |
105 |
106 |
107 | {{with .Type}}{{.}}{{end}} 108 | {{ if not .Required }} 109 | {{ else -}} 110 | Required 111 | {{ end -}} 112 |
113 | {{with .Description}} 114 |
115 | {{.|markdown}} 116 |
117 | {{end}} 118 |
119 |
120 | {{ end }} 121 | 122 | 123 | {{ if $versionSchema.Annotations }} 124 |

Annotations

125 | 126 | {{ range $versionSchema.Annotations }} 127 |
128 |
129 |

{{.Annotation}}

130 |
131 |
132 |
133 | {{with .Release}}{{.}}{{end}} 134 |
135 | {{with .Documentation}} 136 |
137 | {{.|markdown}} 138 |
139 | {{end}} 140 |
141 |
142 | {{ end }} 143 | {{ end }} 144 | 145 |
146 | {{end}} 147 | 148 | {{ else }} 149 |
150 |

We currently cannot show any schema information on this CRD. Sorry for the inconvenience!

151 |

Please refer to the Godoc or source for details.

152 |
153 | {{ end }} 154 | -------------------------------------------------------------------------------- /pkg/output/testdata/test_01.golden: -------------------------------------------------------------------------------- 1 | --- 2 | title: Demo CRD schema reference (group demo.giantswarm.io) 3 | linkTitle: Demo 4 | description: | 5 | Main description of v1alpha2 6 | weight: 100 7 | crd: 8 | name_camelcase: Demo 9 | name_plural: demos 10 | name_singular: demo 11 | group: demo.giantswarm.io 12 | technical_name: demos.demo.giantswarm.io 13 | scope: Namespaced 14 | source_repository: https://github.com/giantswarm/my-repo 15 | source_repository_ref: main 16 | versions: 17 | - v1alpha1 18 | - v1alpha2 19 | topics: 20 | - first-topic 21 | - second-topic 22 | providers: 23 | - aws 24 | - azure 25 | deprecation: 26 | info: This is some deprecation info 27 | replaced_by: 28 | full_name: another.demo.giantswarm.io 29 | short_name: Another 30 | layout: crd 31 | owner: 32 | - first-owner 33 | - second-owner 34 | aliases: 35 | - /reference/cp-k8s-api/demos.demo.giantswarm.io/ 36 | technical_name: demos.demo.giantswarm.io 37 | source_repository: https://github.com/giantswarm/my-repo 38 | source_repository_ref: main 39 | --- 40 | 41 | # Demo 42 | 43 | 44 |

Main description of v1alpha2

45 |
46 |
Full name:
47 |
demos.demo.giantswarm.io
48 |
Group:
49 |
demo.giantswarm.io
50 |
Singular name:
51 |
demo
52 |
Plural name:
53 |
demos
54 |
Scope:
55 |
Namespaced
56 |
Versions:
57 |
v1alpha1
58 |
v1alpha2
59 |
60 | 61 | 62 | 63 |
64 |

Version v1alpha1

65 | 66 | 67 |

Example CR

68 | 69 | ```yaml 70 | This is an example CR 71 | ``` 72 | 73 | 74 |

Properties

75 | 76 | 77 |
78 |
79 |

.first_property

80 |
81 |
82 |
83 | string 84 | Required 85 |
86 | 87 |
88 |

First property

89 | 90 |
91 | 92 |
93 |
94 | 95 | 96 | 97 | 98 | 99 |
100 |
101 |

Version v1alpha2

102 | 103 | 104 | 105 |

Properties

106 | 107 | 108 |
109 |
110 |

.first_property

111 |
112 |
113 |
114 | string 115 | Required 116 |
117 | 118 |
119 |

First property

120 | 121 |
122 | 123 |
124 |
125 | 126 | 127 | 128 | 129 |

Annotations

130 | 131 | 132 |
133 |
134 |

alpha.giantswarm.io/foo

135 |
136 |
137 |
138 | Since v16.0.0 139 |
140 | 141 |
142 |

Here is some annotation documentation.

143 | 144 |
145 | 146 |
147 |
148 | 149 | 150 | 151 |
152 | 153 | 154 | 155 | -------------------------------------------------------------------------------- /renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | // Base config - https://github.com/giantswarm/renovate-presets/blob/main/default.json5 4 | "github>giantswarm/renovate-presets:default.json5", 5 | // Go specific config - https://github.com/giantswarm/renovate-presets/blob/main/lang-go.json5 6 | "github>giantswarm/renovate-presets:lang-go.json5", 7 | ], 8 | } 9 | -------------------------------------------------------------------------------- /templates/crd.template: -------------------------------------------------------------------------------- 1 | --- 2 | title: {{ .Title }} CRD schema reference (group {{ .Group }}) 3 | linkTitle: {{ .Title }} 4 | description: | 5 | {{- if .Description }} 6 | {{ .Description | indent 2 }} 7 | {{- else }} 8 | Custom resource definition (CRD) schema reference page for the {{ .Title }} resource ({{ .NamePlural }}.{{ .Group }}), as part of the Giant Swarm Management API documentation. 9 | {{- end }} 10 | weight: {{ .Weight }} 11 | crd: 12 | name_camelcase: {{ .Title }} 13 | name_plural: {{ .NamePlural }} 14 | name_singular: {{ .NameSingular }} 15 | group: {{ .Group }} 16 | technical_name: {{ .NamePlural }}.{{ .Group }} 17 | scope: {{ .Scope }} 18 | source_repository: {{ .SourceRepository }} 19 | source_repository_ref: {{ .SourceRepositoryRef }} 20 | versions: 21 | {{- range .Versions }} 22 | - {{ . -}} 23 | {{- end }} 24 | topics: 25 | {{- with .Metadata.Topics }} 26 | {{- range . }} 27 | - {{ . -}} 28 | {{- end }} 29 | {{- end }} 30 | {{- with .Metadata.Providers }} 31 | providers: 32 | {{- range . }} 33 | - {{ . -}} 34 | {{- end }} 35 | {{- end }} 36 | {{- with .Metadata.Deprecation }} 37 | deprecation: 38 | {{- with .Info }} 39 | info: {{ . }} 40 | {{- end }} 41 | {{- with .ReplacedBy }} 42 | replaced_by: 43 | full_name: {{ .FullName }} 44 | short_name: {{ .ShortName }} 45 | {{- end }} 46 | {{- end }} 47 | layout: crd 48 | owner: 49 | {{- range .Metadata.Owners }} 50 | - {{ . -}} 51 | {{- end }} 52 | aliases: 53 | - /reference/cp-k8s-api/{{ .NamePlural }}.{{ .Group }}/ 54 | technical_name: {{ .NamePlural }}.{{ .Group }} 55 | source_repository: {{ .SourceRepository }} 56 | source_repository_ref: {{ .SourceRepositoryRef }} 57 | --- 58 | 59 | # {{ .Title }} 60 | 61 | {{- with .Metadata.Deprecation }} 62 |

63 | Deprecation: 64 | {{- with .Info }} 65 | {{ . }} 66 | {{- end }} 67 | {{- with .ReplacedBy }} 68 | This CRD is being replaced by {{ .ShortName }}. 69 | {{- end }} 70 |

71 | {{- end }} 72 | 73 | {{ with .Description }} 74 |

{{ . }}

75 | {{ end -}} 76 | 77 |
78 |
Full name:
79 |
{{ .NamePlural }}.{{ .Group }}
80 |
Group:
81 |
{{ .Group }}
82 |
Singular name:
83 |
{{ .NameSingular }}
84 |
Plural name:
85 |
{{ .NamePlural }}
86 |
Scope:
87 |
{{ .Scope }}
88 |
Versions:
89 |
90 | {{- range .Versions -}} 91 | {{.}} 92 | {{- end -}} 93 |
94 |
95 | 96 | {{ if .VersionSchemas }} 97 | {{ range $versionName, $versionSchema := .VersionSchemas }} 98 |
99 |

Version {{$versionName}}

100 | 101 | {{with .ExampleCR}} 102 |

Example CR

103 | 104 | ```yaml 105 | {{ .|raw -}} 106 | ``` 107 | {{end}} 108 | 109 |

Properties

110 | 111 | {{ range $versionSchema.Properties }} 112 |
113 |
114 |

{{.Path}}

115 |
116 |
117 |
118 | {{with .Type}}{{.}}{{end}} 119 | {{ if not .Required }} 120 | {{ else -}} 121 | Required 122 | {{ end -}} 123 |
124 | {{with .Description}} 125 |
126 | {{.|markdown}} 127 |
128 | {{end}} 129 |
130 |
131 | {{ end }} 132 | 133 | 134 | {{ if .Annotations }} 135 |

Annotations

136 | 137 | {{ range $versionSchema.Annotations }} 138 |
139 |
140 |

{{.Annotation}}

141 |
142 |
143 |
144 | {{with .Release}}{{.}}{{end}} 145 |
146 | {{with .Documentation}} 147 |
148 | {{.|markdown}} 149 |
150 | {{end}} 151 |
152 |
153 | {{ end }} 154 | {{ end }} 155 | 156 |
157 | {{end}} 158 | 159 | {{ else }} 160 |
161 |

We currently cannot show any schema information on this CRD. Sorry for the inconvenience!

162 |

Please refer to the Godoc or source for details.

163 |
164 | {{ end }} 165 | -------------------------------------------------------------------------------- /testdata/case1/config.yaml: -------------------------------------------------------------------------------- 1 | # Configuration for a test case 2 | # where we merge versions from two CRD files. 3 | 4 | template_path: template.gotmpl 5 | output_path: testdata/case1/output 6 | source_repositories: 7 | - url: https://github.com/giantswarm/crd-docs-generator 8 | organization: giantswarm 9 | short_name: crd-docs-generator 10 | commit_reference: main 11 | annotations_paths: 12 | - testdata/case1/annotations 13 | crd_paths: 14 | - testdata/case1/crd 15 | cr_paths: 16 | - testdata/case1/cr 17 | metadata: 18 | examples.example.giantswarm.io: 19 | owner: 20 | - example-owner 21 | topics: 22 | - topic1 23 | - topic2 24 | provider: 25 | - provider1 26 | - provider2 27 | -------------------------------------------------------------------------------- /testdata/case1/crd/crd1.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: examples.example.giantswarm.io 5 | spec: 6 | group: example.giantswarm.io 7 | names: 8 | categories: 9 | - common 10 | - giantswarm 11 | kind: Example 12 | listKind: ExampleList 13 | plural: examples 14 | singular: example 15 | scope: Namespaced 16 | versions: 17 | - name: v1alpha1 18 | served: true 19 | storage: false 20 | schema: 21 | openAPIV3Schema: 22 | description: This is the description of examples.example.giantswarm.io v1alpha1 from file crd1.yaml. 23 | type: object 24 | required: 25 | - spec 26 | properties: 27 | apiVersion: 28 | description: 'APIVersion defines the versioned schema of this representation 29 | of an object. Servers should convert recognized schemas to the latest 30 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 31 | type: string 32 | kind: 33 | description: 'Kind is a string value representing the REST resource this 34 | object represents. Servers may infer this from the endpoint the client 35 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 36 | type: string 37 | metadata: 38 | type: object 39 | spec: 40 | type: object 41 | description: Specifies the desired state of an example resource. 42 | properties: 43 | myObject: 44 | type: object 45 | description: An example object that is nullable, without any required properties. 46 | nullable: true 47 | properties: 48 | mySubObject: 49 | type: object 50 | description: A nullable object as a property of 'myObject' with some required props. 51 | nullable: true 52 | properties: 53 | name: 54 | description: Name is a name of something. 55 | type: string 56 | namespace: 57 | description: Namespace is the namespace of something. 58 | type: string 59 | required: 60 | - name 61 | otherSubObject: 62 | type: object 63 | description: A non-nullable sub object. 64 | nullable: false 65 | properties: 66 | first: 67 | description: First string property. 68 | type: string 69 | second: 70 | description: Second string property. 71 | type: string 72 | - name: v1alpha2 73 | served: true 74 | storage: true 75 | schema: 76 | openAPIV3Schema: 77 | description: This is the description of examples.example.giantswarm.io v1alpha2 from file crd1.yaml. 78 | type: object 79 | required: 80 | - spec 81 | properties: 82 | apiVersion: 83 | description: 'APIVersion defines the versioned schema of this representation 84 | of an object. Servers should convert recognized schemas to the latest 85 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 86 | type: string 87 | kind: 88 | description: 'Kind is a string value representing the REST resource this 89 | object represents. Servers may infer this from the endpoint the client 90 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 91 | type: string 92 | metadata: 93 | type: object 94 | spec: 95 | type: object 96 | description: Specifies the desired state of an example resource. 97 | properties: 98 | myObject: 99 | type: object 100 | description: An example object that is nullable, without any required properties. 101 | nullable: true 102 | properties: 103 | mySubObject: 104 | type: object 105 | description: A nullable object as a property of 'myObject' with some required props. 106 | nullable: true 107 | properties: 108 | name: 109 | description: Name is a name of something. 110 | type: string 111 | namespace: 112 | description: Namespace is the namespace of something. 113 | type: string 114 | required: 115 | - name 116 | -------------------------------------------------------------------------------- /testdata/case1/crd/crd2.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: examples.example.giantswarm.io 5 | spec: 6 | group: example.giantswarm.io 7 | names: 8 | categories: 9 | - common 10 | - giantswarm 11 | kind: Example 12 | listKind: ExampleList 13 | plural: examples 14 | singular: example 15 | scope: Namespaced 16 | versions: 17 | - name: v1alpha2 18 | served: true 19 | storage: false 20 | schema: 21 | openAPIV3Schema: 22 | description: This is the description of examples.example.giantswarm.io v1alpha2 from file crd2.yaml. 23 | type: object 24 | required: 25 | - spec 26 | properties: 27 | apiVersion: 28 | description: 'APIVersion defines the versioned schema of this representation 29 | of an object. Servers should convert recognized schemas to the latest 30 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 31 | type: string 32 | kind: 33 | description: 'Kind is a string value representing the REST resource this 34 | object represents. Servers may infer this from the endpoint the client 35 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 36 | type: string 37 | metadata: 38 | type: object 39 | spec: 40 | type: object 41 | description: Specifies the desired state of an example resource. 42 | properties: 43 | myObject: 44 | type: object 45 | description: An example object that is nullable, without any required properties. 46 | nullable: true 47 | properties: 48 | mySubObject: 49 | type: object 50 | description: A nullable object as a property of 'myObject' with some required props. 51 | nullable: true 52 | properties: 53 | name: 54 | description: Name is a name of something. 55 | type: string 56 | namespace: 57 | description: Namespace is the namespace of something. 58 | type: string 59 | required: 60 | - name 61 | otherSubObject: 62 | type: object 63 | description: A non-nullable sub object. 64 | nullable: false 65 | properties: 66 | first: 67 | description: First string property. 68 | type: string 69 | second: 70 | description: Second string property. 71 | type: string 72 | - name: v1alpha3 73 | served: true 74 | storage: true 75 | schema: 76 | openAPIV3Schema: 77 | description: This is the description of examples.example.giantswarm.io v1alpha3 from file crd2.yaml. 78 | type: object 79 | required: 80 | - spec 81 | properties: 82 | apiVersion: 83 | description: 'APIVersion defines the versioned schema of this representation 84 | of an object. Servers should convert recognized schemas to the latest 85 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 86 | type: string 87 | kind: 88 | description: 'Kind is a string value representing the REST resource this 89 | object represents. Servers may infer this from the endpoint the client 90 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 91 | type: string 92 | metadata: 93 | type: object 94 | spec: 95 | type: object 96 | description: Specifies the desired state of an example resource. 97 | properties: 98 | myObject: 99 | type: object 100 | description: An example object that is nullable, without any required properties. 101 | nullable: true 102 | properties: 103 | mySubObject: 104 | type: object 105 | description: A nullable object as a property of 'myObject' with some required props. 106 | nullable: true 107 | properties: 108 | name: 109 | description: Name is a name of something. 110 | type: string 111 | namespace: 112 | description: Namespace is the namespace of something. 113 | type: string 114 | required: 115 | - name 116 | -------------------------------------------------------------------------------- /testdata/case1/output.golden: -------------------------------------------------------------------------------- 1 | description: This is the description of examples.example.giantswarm.io v1alpha2 from file crd2.yaml. 2 | group: example.giantswarm.io 3 | name_plural: examples 4 | name_singular: example 5 | scope: Namespaced 6 | source_repository_ref: main 7 | source_repository: https://github.com/giantswarm/crd-docs-generator 8 | title: Example 9 | versions: [v1alpha2 v1alpha3] 10 | weight: 100 11 | 12 | Metadata: 13 | 14 | topics: [topic1 topic2] 15 | providers: [provider1 provider2] 16 | 17 | 18 | 19 | 20 | 21 | Version v1alpha2 22 | 23 | 24 | 25 | Properties 26 | 27 | 28 | 29 | Depth: 0 30 | Path: .apiVersion 31 | string 32 | 33 | 34 | 35 |

APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources

36 | 37 | 38 | Depth: 0 39 | Path: .kind 40 | string 41 | 42 | 43 | 44 |

Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds

45 | 46 | 47 | Depth: 0 48 | Path: .metadata 49 | object 50 | 51 | 52 | 53 | 54 | Depth: 0 55 | Path: .spec 56 | object 57 | Required 58 | 59 | 60 |

Specifies the desired state of an example resource.

61 | 62 | 63 | Depth: 1 64 | Path: .spec.myObject 65 | object 66 | 67 | 68 | 69 |

An example object that is nullable, without any required properties.

70 | 71 | 72 | Depth: 2 73 | Path: .spec.myObject.mySubObject 74 | object 75 | 76 | 77 | 78 |

A nullable object as a property of ‘myObject’ with some required props.

79 | 80 | 81 | Depth: 3 82 | Path: .spec.myObject.mySubObject.name 83 | string 84 | Required 85 | 86 | 87 |

Name is a name of something.

88 | 89 | 90 | Depth: 3 91 | Path: .spec.myObject.mySubObject.namespace 92 | string 93 | 94 | 95 | 96 |

Namespace is the namespace of something.

97 | 98 | 99 | Depth: 2 100 | Path: .spec.myObject.otherSubObject 101 | object 102 | 103 | 104 | 105 |

A non-nullable sub object.

106 | 107 | 108 | Depth: 3 109 | Path: .spec.myObject.otherSubObject.first 110 | string 111 | 112 | 113 | 114 |

First string property.

115 | 116 | 117 | Depth: 3 118 | Path: .spec.myObject.otherSubObject.second 119 | string 120 | 121 | 122 | 123 |

Second string property.

124 | 125 | 126 | 127 | 128 | Version v1alpha3 129 | 130 | 131 | 132 | Properties 133 | 134 | 135 | 136 | Depth: 0 137 | Path: .apiVersion 138 | string 139 | 140 | 141 | 142 |

APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources

143 | 144 | 145 | Depth: 0 146 | Path: .kind 147 | string 148 | 149 | 150 | 151 |

Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds

152 | 153 | 154 | Depth: 0 155 | Path: .metadata 156 | object 157 | 158 | 159 | 160 | 161 | Depth: 0 162 | Path: .spec 163 | object 164 | Required 165 | 166 | 167 |

Specifies the desired state of an example resource.

168 | 169 | 170 | Depth: 1 171 | Path: .spec.myObject 172 | object 173 | 174 | 175 | 176 |

An example object that is nullable, without any required properties.

177 | 178 | 179 | Depth: 2 180 | Path: .spec.myObject.mySubObject 181 | object 182 | 183 | 184 | 185 |

A nullable object as a property of ‘myObject’ with some required props.

186 | 187 | 188 | Depth: 3 189 | Path: .spec.myObject.mySubObject.name 190 | string 191 | Required 192 | 193 | 194 |

Name is a name of something.

195 | 196 | 197 | Depth: 3 198 | Path: .spec.myObject.mySubObject.namespace 199 | string 200 | 201 | 202 | 203 |

Namespace is the namespace of something.

204 | 205 | 206 | 207 | -------------------------------------------------------------------------------- /testdata/case1/output/examples.example.giantswarm.io.md: -------------------------------------------------------------------------------- 1 | description: This is the description of examples.example.giantswarm.io v1alpha2 from file crd2.yaml. 2 | group: example.giantswarm.io 3 | name_plural: examples 4 | name_singular: example 5 | scope: Namespaced 6 | source_repository_ref: main 7 | source_repository: https://github.com/giantswarm/crd-docs-generator 8 | title: Example 9 | versions: [v1alpha2 v1alpha3] 10 | weight: 100 11 | 12 | Metadata: 13 | 14 | topics: [topic1 topic2] 15 | providers: [provider1 provider2] 16 | 17 | 18 | 19 | 20 | 21 | Version v1alpha2 22 | 23 | 24 | 25 | Properties 26 | 27 | 28 | 29 | Depth: 0 30 | Path: .apiVersion 31 | string 32 | 33 | 34 | 35 |

APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources

36 | 37 | 38 | Depth: 0 39 | Path: .kind 40 | string 41 | 42 | 43 | 44 |

Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds

45 | 46 | 47 | Depth: 0 48 | Path: .metadata 49 | object 50 | 51 | 52 | 53 | 54 | Depth: 0 55 | Path: .spec 56 | object 57 | Required 58 | 59 | 60 |

Specifies the desired state of an example resource.

61 | 62 | 63 | Depth: 1 64 | Path: .spec.myObject 65 | object 66 | 67 | 68 | 69 |

An example object that is nullable, without any required properties.

70 | 71 | 72 | Depth: 2 73 | Path: .spec.myObject.mySubObject 74 | object 75 | 76 | 77 | 78 |

A nullable object as a property of ‘myObject’ with some required props.

79 | 80 | 81 | Depth: 3 82 | Path: .spec.myObject.mySubObject.name 83 | string 84 | Required 85 | 86 | 87 |

Name is a name of something.

88 | 89 | 90 | Depth: 3 91 | Path: .spec.myObject.mySubObject.namespace 92 | string 93 | 94 | 95 | 96 |

Namespace is the namespace of something.

97 | 98 | 99 | Depth: 2 100 | Path: .spec.myObject.otherSubObject 101 | object 102 | 103 | 104 | 105 |

A non-nullable sub object.

106 | 107 | 108 | Depth: 3 109 | Path: .spec.myObject.otherSubObject.first 110 | string 111 | 112 | 113 | 114 |

First string property.

115 | 116 | 117 | Depth: 3 118 | Path: .spec.myObject.otherSubObject.second 119 | string 120 | 121 | 122 | 123 |

Second string property.

124 | 125 | 126 | 127 | 128 | Version v1alpha3 129 | 130 | 131 | 132 | Properties 133 | 134 | 135 | 136 | Depth: 0 137 | Path: .apiVersion 138 | string 139 | 140 | 141 | 142 |

APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources

143 | 144 | 145 | Depth: 0 146 | Path: .kind 147 | string 148 | 149 | 150 | 151 |

Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds

152 | 153 | 154 | Depth: 0 155 | Path: .metadata 156 | object 157 | 158 | 159 | 160 | 161 | Depth: 0 162 | Path: .spec 163 | object 164 | Required 165 | 166 | 167 |

Specifies the desired state of an example resource.

168 | 169 | 170 | Depth: 1 171 | Path: .spec.myObject 172 | object 173 | 174 | 175 | 176 |

An example object that is nullable, without any required properties.

177 | 178 | 179 | Depth: 2 180 | Path: .spec.myObject.mySubObject 181 | object 182 | 183 | 184 | 185 |

A nullable object as a property of ‘myObject’ with some required props.

186 | 187 | 188 | Depth: 3 189 | Path: .spec.myObject.mySubObject.name 190 | string 191 | Required 192 | 193 | 194 |

Name is a name of something.

195 | 196 | 197 | Depth: 3 198 | Path: .spec.myObject.mySubObject.namespace 199 | string 200 | 201 | 202 | 203 |

Namespace is the namespace of something.

204 | 205 | 206 | 207 | -------------------------------------------------------------------------------- /testdata/case1/template.gotmpl: -------------------------------------------------------------------------------- 1 | description: {{ .Description }} 2 | group: {{ .Group }} 3 | name_plural: {{ .NamePlural }} 4 | name_singular: {{ .NameSingular }} 5 | scope: {{ .Scope }} 6 | source_repository_ref: {{ .SourceRepositoryRef }} 7 | source_repository: {{ .SourceRepository }} 8 | title: {{ .Title }} 9 | versions: {{ .Versions }} 10 | weight: {{ .Weight }} 11 | 12 | Metadata: 13 | 14 | topics: {{ .Metadata.Topics }} 15 | providers: {{ .Metadata.Providers }} 16 | {{ with .Metadata.Deprecation }} 17 | deprecation: {{ . }} 18 | {{ with .ReplacedBy }} 19 | replaced by: {{ . }} 20 | {{- end }} 21 | {{- end }} 22 | 23 | {{ if .VersionSchemas }} 24 | {{ range $versionName, $versionSchema := .VersionSchemas }} 25 | 26 | Version {{$versionName}} 27 | 28 | {{ with .ExampleCR }} 29 | Example CR 30 | 31 | ```yaml 32 | {{ .|raw }} 33 | ``` 34 | {{ end }} 35 | 36 | Properties 37 | 38 | {{ range $versionSchema.Properties }} 39 | 40 | Depth: {{ .Depth }} 41 | Path: {{ .Path }} 42 | {{ with .Type }}{{ . }}{{ end}} 43 | {{ if not .Required }}{{ else }}Required{{ end }} 44 | 45 | {{ with .Description}} 46 | {{ .|markdown }} 47 | {{- end }} 48 | {{- end }} 49 | 50 | {{ if .Annotations }} 51 | Annotations 52 | 53 | {{ range $versionSchema.Annotations }} 54 | Annotation name: {{ .Annotation }} 55 | {{ with .Release }}{{ . }}{{ end }} 56 | {{ with .Documentation}}{{.|markdown}}{{- end }} 57 | {{- end }} 58 | {{- end }} 59 | {{- end }} 60 | {{- end }} 61 | --------------------------------------------------------------------------------