diff --git a/.boilerplate.json b/.boilerplate.json new file mode 100644 index 00000000..a2932dea --- /dev/null +++ b/.boilerplate.json @@ -0,0 +1,12 @@ +{ + "dirs_to_skip" : [ + "hack/boilerplate/testdata/", + "hack/tools/", + "vendor", + ".cache", + ".pkg" + ], + "not_generated_files_to_skip" : [ + "hack/boilerplate/boilerplate.py" + ] +} \ No newline at end of file diff --git a/.builder-image-version.txt b/.builder-image-version.txt new file mode 100644 index 00000000..6e8bf73a --- /dev/null +++ b/.builder-image-version.txt @@ -0,0 +1 @@ +0.1.0 diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..1b35c90c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,23 @@ +--- +name: 'Bug report' +about: Tell us about a problem you are experiencing. + +--- + +/kind bug + +**What steps did you take and what happened:** +[A clear and concise description of what the bug is.] + + +**What did you expect to happen:** + + +**Anything else you would like to add:** +[Miscellaneous information that will assist in solving the issue.] + + +**Environment:** + +- csctl-plugin-openstack version: (use `csctl-plugin-openstack version`) +- OS (e.g. from `/etc/os-release`): diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..34a62051 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,19 @@ +--- +name: 'Feature request' +about: Suggest an idea for this project. + +--- + +/kind feature + +**Describe the solution you'd like** +[A clear and concise description of what you want to happen.] + + +**Anything else you would like to add:** +[Miscellaneous information that will assist in solving the issue.] + + +**Environment:** + +- csctl-plugin-openstack version: (use `csctl-plugin-openstack version`) diff --git a/.github/ISSUE_TEMPLATE/proposal.md b/.github/ISSUE_TEMPLATE/proposal.md new file mode 100644 index 00000000..454f1293 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/proposal.md @@ -0,0 +1,23 @@ +--- +name: Enhancement Proposal +about: Propose larger efforts, breaking changes, or new features + +--- + +***Goals*** +1. Goal 1 +2. Goal 2 + +***Non-Goals/Future Work*** +1. Non-Goal 1 +1. Non-Goal 2 + +**User Story** + +As a [developer/user/operator] I would like to [high level description] for [reasons] + +**Detailed Description** + +[A clear and concise description of what you want to happen.] + +/kind proposal diff --git a/.github/actions/setup-go/action.yaml b/.github/actions/setup-go/action.yaml new file mode 100644 index 00000000..a6bc7e61 --- /dev/null +++ b/.github/actions/setup-go/action.yaml @@ -0,0 +1,30 @@ +name: "Setup Go" +description: "Setup Go" +runs: + using: "composite" + steps: + - name: Install go + uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 + with: + go-version-file: "go.mod" + cache: true + cache-dependency-path: go.sum + - id: go-cache-paths + shell: bash + run: | + echo "go-build=$(go env GOCACHE)" >> $GITHUB_OUTPUT + echo "go-mod=$(go env GOMODCACHE)" >> $GITHUB_OUTPUT + - name: Go Mod Cache + uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4 + with: + path: ${{ steps.go-cache-paths.outputs.go-mod }} + key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go-mod- + - name: Go Build Cache + uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4 + with: + path: ${{ steps.go-cache-paths.outputs.go-build }} + key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go-build- diff --git a/.github/labeler.yaml b/.github/labeler.yaml new file mode 100644 index 00000000..3abbec53 --- /dev/null +++ b/.github/labeler.yaml @@ -0,0 +1,22 @@ +--- +area/code: + - changed-files: + - any-glob-to-any-file: "controllers/**/*" + - any-glob-to-any-file: "pkg/**/*" +area/api: + - changed-files: + - any-glob-to-any-file: "api/**/*" + - any-glob-to-any-file: "config/crd/**/*" +area/github: + - changed-files: + - any-glob-to-any-file: ".github/**/*" +area/hack: + - changed-files: + - any-glob-to-any-file: "hack/**/*" + - any-glob-to-any-file: "Makefile" +area/test: + - changed-files: + - any-glob-to-any-file: "test/**/*" +area/templates: + - changed-files: + - any-glob-to-any-file: "templates/**/*" diff --git a/.github/labels.yaml b/.github/labels.yaml new file mode 100644 index 00000000..3f5706ff --- /dev/null +++ b/.github/labels.yaml @@ -0,0 +1,80 @@ +--- +# Area +- name: area/code + color: "72ccf3" + description: >- + Changes made in the code directory +- name: area/api + color: "72ccf3" + description: >- + Changes made in the api directory +- name: area/github + color: "72ccf3" + description: >- + Changes made in the github directory +- name: area/hack + color: "72ccf3" + description: >- + Changes made in the hack directory +- name: area/test + color: "72ccf3" + description: >- + Changes made in the test directory +- name: area/templates + color: "72ccf3" + description: >- + Changes made in the templates directory +# Update +- name: update/container + color: "ffc300" +- name: update/github-action + color: "ffc300" +- name: update/helm + color: "ffc300" +- name: update/go + color: "ffc300" +# Semantic Type +- name: type/patch + color: "FFEC19" +- name: type/minor + color: "FF9800" +- name: type/major + color: "F6412D" +# Size +- name: size/XS + color: "009900" + description: >- + Denotes a PR that changes 0-20 lines, ignoring generated files. +- name: size/S + color: "77bb00" + description: >- + Denotes a PR that changes 20-50 lines, ignoring generated files. +- name: size/M + color: "eebb00" + description: >- + Denotes a PR that changes 50-200 lines, ignoring generated files. +- name: size/L + color: "ee9900" + description: >- + Denotes a PR that changes 200-800 lines, ignoring generated files. +- name: size/XL + color: "ee5500" + description: >- + Denotes a PR that changes 800-2000 lines, ignoring generated files. +- name: size/XXL + color: "ee0000" + description: >- + Denotes a PR that changes 2000+ lines, ignoring generated files. +# Uncategorized +- name: bug + color: "ee0701" +- name: do-not-merge + color: "ee0701" +- name: docs + color: "F4D1B7" +- name: enhancement + color: "84b6eb" +- name: link-checker + color: "7B55D7" +- name: question + color: "cc317c" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..5aec73ec --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,19 @@ + + + + +**What this PR does / why we need it**: + +**Which issue(s) this PR fixes** *(optional, in `fixes #(, fixes #, ...)` format, will close the issue(s) when PR gets merged)*: +Fixes # + +**Special notes for your reviewer**: + +_Please confirm that if this PR changes any image versions, then that's the sole change this PR makes._ + +**TODOs**: + +- [ ] squash commits +- [ ] include documentation +- [ ] add unit tests + diff --git a/.github/renovate.json5 b/.github/renovate.json5 new file mode 100644 index 00000000..32208fb4 --- /dev/null +++ b/.github/renovate.json5 @@ -0,0 +1,47 @@ +{ + extends: [ + ":dependencyDashboard", + ":semanticPrefixFixDepsChoreOthers", + ":autodetectRangeStrategy", + ":disableRateLimiting", + ":semanticCommits", + "helpers:pinGitHubActionDigests", + "github>whitesource/merge-confidence:beta", + "github>SovereignCloudStack/csctl-plugin-openstack//.github/renovate/commitMessage.json5", + "github>SovereignCloudStack/csctl-plugin-openstack//.github/renovate/approval.json5", + "github>SovereignCloudStack/csctl-plugin-openstack//.github/renovate/golang.json5", + "github>SovereignCloudStack/csctl-plugin-openstack//.github/renovate/groups.json5", + "github>SovereignCloudStack/csctl-plugin-openstack//.github/renovate/labels.json5", + "github>SovereignCloudStack/csctl-plugin-openstack//.github/renovate/regexManagers.json5" + ], + platform: "github", + baseBranches: ["main"], + onboarding: false, + requireConfig: "ignored", + timezone: "Europe/Berlin", + // repo config + repositories: ["SovereignCloudStack/csctl-plugin-openstack"], + ignorePaths: [ + "**/vendor/**", + "**/test/**", + "**/tests/**" + ], + username: "cluster-stack-bot[bot]", + gitAuthor: "cluster-stack-bot[bot] <143188378+cluster-stack-bot[bot]@users.noreply.github.com>", + // PR config + dependencyDashboardTitle: "Dependency Dashboard 🤖", + dependencyDashboardHeader: "", + prFooter: "", + suppressNotifications: ["prIgnoreNotification"], + rebaseWhen: "conflicted", + commitBodyTable: true, + prHourlyLimit: 1, + printConfig: true, + pruneStaleBranches: true, + allowPostUpgradeCommandTemplating: true, + separateMajorMinor: true, + separateMultipleMajor: true, + separateMinorPatch: true, + enabledManagers: ["dockerfile", "gomod", "github-actions", "regex"], + recreateClosed: true, +} diff --git a/.github/renovate/approval.json5 b/.github/renovate/approval.json5 new file mode 100644 index 00000000..757cfba8 --- /dev/null +++ b/.github/renovate/approval.json5 @@ -0,0 +1,11 @@ +{ + packageRules: [ + { + matchUpdateTypes: ["major", "minor"], + matchManagers: ["gomod"], + matchDepTypes: ["golang"], + description: "Ask for approval for golang updates", + dependencyDashboardApproval: true, + }, + ], +} diff --git a/.github/renovate/commitMessage.json5 b/.github/renovate/commitMessage.json5 new file mode 100644 index 00000000..0faa3fd3 --- /dev/null +++ b/.github/renovate/commitMessage.json5 @@ -0,0 +1,18 @@ +{ + "commitMessagePrefix": ":seedling: ", + "commitMessageTopic": "{{depName}}", + "commitMessageExtra": "to {{newVersion}}", + "commitMessageSuffix": "", + "group": { commitMessageTopic: "{{{groupName}}} group" }, + "packageRules": [ + { + "matchDatasources": ["helm"], + "commitMessageTopic": "chart {{depName}}" + }, + { + "matchDatasources": ["docker"], + "commitMessageTopic": "image {{depName}}", + "commitMessageExtra": "to {{#if isSingleVersion}}v{{{newVersion}}}{{else}}{{{newValue}}}{{/if}}" + } + ] +} \ No newline at end of file diff --git a/.github/renovate/golang.json5 b/.github/renovate/golang.json5 new file mode 100644 index 00000000..29424391 --- /dev/null +++ b/.github/renovate/golang.json5 @@ -0,0 +1,31 @@ +{ + golang: { + postUpdateOptions: ["gomodTidy", "gomodUpdateImportPaths"], + }, + // https://docs.renovatebot.com/configuration-options/#constraints + "constraints": { + "go": "1.21" + }, + packageRules: [ + { + description: "Disable Golang update for major and minor versions", + matchManagers: ["dockerfile"], + matchDepNames: ["docker.io/library/golang"], + matchUpdateTypes: ["major", "minor"], + enabled: false, + }, + { + description: "Disable slim-sprig", + matchManagers: ["gomod"], + matchDepNames: ["github.com/go-task/slim-sprig"], + matchPaths: ["hack/tools/**"], + enabled: false, + }, + { + description: "Disable update k8s packages", + matchManagers: ["gomod"], + matchDepNames: ["k8s.io/api", "k8s.io/apimachinery", "k8s.io/apiserver", "k8s.io/client-go", "k8s.io/kubectl", "k8s.io/code-generator"], + enabled: false, + }, + ], +} diff --git a/.github/renovate/groups.json5 b/.github/renovate/groups.json5 new file mode 100644 index 00000000..510310da --- /dev/null +++ b/.github/renovate/groups.json5 @@ -0,0 +1,66 @@ +{ + packageRules: [ + { + description: "Update Builder Image", + groupName: "Builder Image", + groupSlug: "cpo-builder-image", + commitMessageTopic: "Builder Image group", + matchPaths: ["images/builder/**"], + separateMajorMinor: false, + separateMultipleMajor: false, + separateMinorPatch: false, + schedule: ["on the first day of the month"], + }, + { + description: "Update Makefile", + groupName: "Makefile", + matchManagers: ["regex"], + separateMajorMinor: false, + separateMultipleMajor: false, + separateMinorPatch: false, + matchFiles: ["Makefile"], + commitMessageTopic: "Makefile group", + groupSlug: "makefile", + }, + { + description: "Update Github Actions", + groupName: "github-actions", + matchManagers: ["github-actions"], + matchUpdateTypes: ["major", "minor", "patch", "digest", "pin", "pinDigest"], + pinDigests: true, + commitMessageTopic: "Github Actions group", + groupSlug: "github-actions", + schedule: ["on monday"], + }, + { + description: "Update Bot Schedule", + matchManagers: ["github-actions"], + matchUpdateTypes: ["major", "minor", "patch"], + matchPackageNames: ["renovatebot/github-action"], + schedule: ["on the first day of the month"], + }, + { + description: "Update Go Dev Dependencies", + groupName: "Update Go Dev Dependencies", + matchManagers: ["gomod"], + matchPaths: ["hack/tools/**"], + commitMessageTopic: "Go Dev Dependencies group", + groupSlug: "golang-devs-deps", + }, + { + description: "Update Golang Dependencies", + groupName: "Update Golang Dependencies", + matchManagers: ["gomod"], + ignorePaths: ["hack/tools/**"], + commitMessageTopic: "Golang Dependencies group", + groupSlug: "golang-deps", + }, + { + description: "disable update of builder image", + matchManagers: ["github-actions"], + matchUpdateTypes: ["major", "minor", "patch"], + matchPackageNames: ["ghcr.io/sovereigncloudstack/builder"], + enabled: false, + }, + ] +} diff --git a/.github/renovate/labels.json5 b/.github/renovate/labels.json5 new file mode 100644 index 00000000..815dcebb --- /dev/null +++ b/.github/renovate/labels.json5 @@ -0,0 +1,32 @@ +{ + "packageRules": [ + { + "matchUpdateTypes": ["major"], + "labels": ["type/major"] + }, + { + "matchUpdateTypes": ["minor"], + "labels": ["type/minor"] + }, + { + "matchUpdateTypes": ["patch"], + "labels": ["type/patch"] + }, + { + "matchDatasources": ["helm"], + "addLabels": ["update/helm"] + }, + { + "matchDatasources": ["docker"], + "addLabels": ["update/container"] + }, + { + "matchManagers": ["github-actions"], + "addLabels": ["update/github-action"] + }, + { + "matchDatasources": ["go"], + "addLabels": ["update/go"] + } + ] + } \ No newline at end of file diff --git a/.github/renovate/regexManagers.json5 b/.github/renovate/regexManagers.json5 new file mode 100644 index 00000000..cf56ee16 --- /dev/null +++ b/.github/renovate/regexManagers.json5 @@ -0,0 +1,19 @@ +{ + "regexManagers": [ + { + "fileMatch": [".yaml$", ".yml$", "Makefile", "(^|/|\.)Dockerfile$", "(^|/)Dockerfile[^/]*$"], + "matchStrings": [ + ".*(@|=|==|:\\s)(?[v0-9.-]+)\\s#\\supdate: datasource=(?.*?) depName=(?.*?)( extractVersion=(?.+?))?( versioning=(?.*?))?\\s" + ], + "extractVersionTemplate":"{{#if extractVersion}}{{{extractVersion}}}{{/if}}", + "versioningTemplate": "{{#if versioning}}{{{versioning}}}{{else}}semver{{/if}}" + }, + { + "fileMatch": ["(^|/|\.)Dockerfile$", "(^|/)Dockerfile[^/]*$"], + "matchStrings": [ + "#\\s*update:\\s*datasource=(?.*?) depName=(?.*?)( versioning=(?.*?))?\\sENV .*?_VERSION=\"(?.*)\"\\s" + ], + "versioningTemplate": "{{#if versioning}}{{{versioning}}}{{else}}semver{{/if}}" + }, + ] +} \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..a8540456 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,22 @@ +name: Build csctl-plugin-openstack binary +# yamllint disable rule:line-length +on: # yamllint disable-line rule:truthy + push: + branches: + - main + +jobs: + manager-image: + name: Build and push manager image + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + with: + fetch-depth: 0 + - uses: ./.github/actions/setup-go + + # Load Golang cache build from GitHub + - name: build go binary + run: | + go build -o csctl-plugin-openstack main.go diff --git a/.github/workflows/pr-lint.yml b/.github/workflows/pr-lint.yml new file mode 100644 index 00000000..f26e099d --- /dev/null +++ b/.github/workflows/pr-lint.yml @@ -0,0 +1,52 @@ +name: "Lint Pull Request" +on: # yamllint disable-line rule:truthy + workflow_dispatch: + pull_request_target: + types: [opened, edited, synchronize, reopened, ready_for_review] + paths: + - "**.go" + - "**go.mod" + - "**go.sum" + - "/**/*." + - ".github/actions/**/*" + - ".github/workflows/e2e-*" + - ".github/workflows/pr-*" + - "!**/vendor/**" +# yamllint disable rule:line-length +jobs: + pr-lint: + name: "Lint Pull Request" + if: github.event_name != 'pull_request_target' || !github.event.pull_request.draft + runs-on: ubuntu-latest + container: + image: ghcr.io/sovereigncloudstack/builder:0.1.0 + credentials: + username: ${{ github.actor }} + password: ${{ secrets.github_token }} + steps: + - name: Checkout repository + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - name: Fixup git permissions + # https://github.com/actions/checkout/issues/766 + shell: bash + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + + - name: Verify Golang Modules + run: make BUILD_IN_CONTAINER=false generate-modules-ci + + - name: Lint Golang Code + run: make BUILD_IN_CONTAINER=false lint-golang-ci + + - name: Link Checker + env: + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} + run: make BUILD_IN_CONTAINER=false lint-links + + - name: Lint YAML + run: make BUILD_IN_CONTAINER=false lint-yaml-ci + + - name: Lint Dockerfile + run: make BUILD_IN_CONTAINER=false lint-dockerfile diff --git a/.github/workflows/pr-verify.yml b/.github/workflows/pr-verify.yml new file mode 100644 index 00000000..a5322561 --- /dev/null +++ b/.github/workflows/pr-verify.yml @@ -0,0 +1,70 @@ +name: Verify Pull Request +on: # yamllint disable-line rule:truthy + pull_request_target: + types: [opened, edited, synchronize, reopened, ready_for_review] +# yamllint disable rule:line-length +jobs: + pr-verify: + runs-on: ubuntu-latest + name: Verify Pull Request + if: github.event_name != 'pull_request_target' || !github.event.pull_request.draft + steps: + - name: Verifier action + id: verifier + uses: kubernetes-sigs/kubebuilder-release-tools@012269a88fa4c034a0acf1ba84c26b195c0dbab4 # v0.4.3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + + - name: Checkout repository + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - name: Verify Boilerplate + run: make verify-boilerplate + + - name: Verify Shellcheck + run: make verify-shellcheck + + - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4 + with: + node-version: "18" + - name: Install renovate + run: npm i -g renovate@35.54.0 # TODO update this via renovatebot + + - name: Validate config + run: | + for file in $(find . -name "*.json5"); do + renovate-config-validator ${file} + done + + - name: Generate Token + uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a # v2 + id: generate-token + with: + app_id: ${{ secrets.SCS_APP_ID }} + private_key: ${{ secrets.SCS_APP_PRIVATE_KEY }} + - name: Generate Size + uses: pascalgn/size-label-action@37a5ad4ae20ea8032abf169d953bcd661fd82cd3 # v0.5.0 + env: + GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }} + with: + sizes: > + { + "0": "XS", + "20": "S", + "50": "M", + "200": "L", + "800": "XL", + "2000": "XXL" + } + - name: Generate Labels + uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5 + with: + configuration-path: .github/labeler.yaml + repo-token: ${{ steps.generate-token.outputs.token }} + - name: Sync Labels + uses: EndBug/label-sync@52074158190acb45f3077f9099fea818aa43f97a # v2 + with: + config-file: .github/labels.yaml + token: ${{ steps.generate-token.outputs.token }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..55a7c5fd --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,30 @@ +# .github/workflows/release.yml +name: goreleaser + +on: + push: + tags: + - "v*" + +permissions: + contents: write + +jobs: + goreleaser: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + with: + fetch-depth: 0 + - run: git fetch --force --tags + - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5 + with: + go-version: stable + + - uses: goreleaser/goreleaser-action@7ec5c2b0c6cdda6e8bbb49444bc797dd33d74dd8 # v5 + with: + distribution: goreleaser + version: latest + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/schedule-link-checker.yml b/.github/workflows/schedule-link-checker.yml new file mode 100644 index 00000000..d2098a36 --- /dev/null +++ b/.github/workflows/schedule-link-checker.yml @@ -0,0 +1,49 @@ +name: "Schedule - Check links" +on: # yamllint disable-line rule:truthy + workflow_dispatch: + schedule: + - cron: "0 0 1 * *" +# yamllint disable rule:line-length +jobs: + link-checker: + name: Link Checker + runs-on: ubuntu-latest + if: github.repository == 'SovereignCloudStack/csctl-plugin-openstack' + steps: + - name: Checkout + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: Generate Token + uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a # v2 + id: generate-token + with: + app_id: ${{ secrets.SCS_APP_ID }} + private_key: ${{ secrets.SCS_APP_PRIVATE_KEY }} + + - name: Link Checker + uses: lycheeverse/lychee-action@c053181aa0c3d17606addfe97a9075a32723548a # v1.9.3 + id: lychee + env: + GITHUB_TOKEN: "${{ steps.generate-token.outputs.token }}" + with: + args: --config .lychee.toml ./*.md ./docs/**/*.md + output: ./lychee.md + format: markdown + + - name: Find Link Checker Issue + id: link-checker-issue + uses: micalevisk/last-issue-action@0d40124cc99ac8601c2516007f0c98ef3d27537b # v2 + with: + state: open + labels: | + link-checker + + - name: Update Issue + uses: peter-evans/create-issue-from-file@24452a72d85239eacf1468b0f1982a9f3fec4c94 # v5 + with: + title: Link Checker Dashboard + issue-number: "${{ steps.link-checker-issue.outputs.issue_number }}" + content-filepath: ./lychee.md + token: "${{ steps.generate-token.outputs.token }}" + labels: | + link-checker diff --git a/.github/workflows/schedule-update-bot.yaml b/.github/workflows/schedule-update-bot.yaml new file mode 100644 index 00000000..73b66d54 --- /dev/null +++ b/.github/workflows/schedule-update-bot.yaml @@ -0,0 +1,56 @@ +name: Schedule - Update Bot +on: # yamllint disable-line rule:truthy + workflow_dispatch: + inputs: + dryRun: + description: "Dry-Run" + default: "false" + required: false + logLevel: + description: "Log-Level" + default: "debug" + required: false + schedule: + - cron: "0 11 * * *" + push: + branches: + - main + paths: + - ".github/renovate.json5" + - ".github/renovate/**.json" +env: + LOG_LEVEL: info + DRY_RUN: false + RENOVATE_CONFIG_FILE: .github/renovate.json5 +# yamllint disable rule:line-length +jobs: + update-bot: + if: github.repository == 'SovereignCloudStack/csctl-plugin-openstack' + name: Renovate + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: Generate Token + uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a # v2 + id: generate-token + with: + app_id: ${{ secrets.SCS_APP_ID }} + private_key: ${{ secrets.SCS_APP_PRIVATE_KEY }} + + - name: Override default config from dispatch variables + run: | + echo "DRY_RUN=${{ github.event.inputs.dryRun || env.DRY_RUN }}" >> "$GITHUB_ENV" + echo "LOG_LEVEL=${{ github.event.inputs.logLevel || env.LOG_LEVEL }}" >> "$GITHUB_ENV" + + - name: Renovate + uses: renovatebot/github-action@a6e57359b32af9a54d5b3b6603011f50629a0a05 # v40.1.2 + env: + RENOVATE_HOST_RULES: '[{"hostType": "docker", "matchHost": "ghcr.io", "username": "${{ github.actor }}", "password": "${{ secrets.GITHUB_TOKEN }}" }]' + RENOVATE_ALLOWED_POST_UPGRADE_COMMANDS: '[".*"]' + BUILDER_IMAGE: 'ghcr.io/sovereigncloudstack/builder' + with: + configurationFile: ${{ env.RENOVATE_CONFIG_FILE }} + token: "x-access-token:${{ steps.generate-token.outputs.token }}" + mount-docker-socket: "true" diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..a7a4d4ac --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +.envrc +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +bin +hack/tools/bin +testbin/* +temp +# Test binary, build with `go test -c` +*.test +.coverage +.cache +.reports +# build and release +dist +csctl-plugin-openstack +tmp/ +releases/ diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 00000000..443d2024 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,290 @@ +linters: + disable-all: true + enable: + - asasalint + - asciicheck + - bidichk + - bodyclose + - containedctx + - contextcheck + - durationcheck + - errchkjson + - errname + - errorlint + - exhaustive + - exportloopref + - forcetypeassert + - gci + - gocritic + - godot + - gofmt + - gofumpt + - goimports + - goprintffuncname + - gosec + - gosimple + - govet + - importas + - ineffassign + - loggercheck + - makezero + - misspell + - nakedret + - nilerr + - noctx + - nolintlint + - nosprintfhostport + - prealloc + - predeclared + - reassign + - revive + - rowserrcheck + - staticcheck + - stylecheck + - tagliatelle + - thelper + - tparallel + - typecheck + - unconvert + - usestdlibvars + - unused + - wastedassign + - wrapcheck + +linters-settings: + godot: + # declarations - for top level declaration comments (default); + # toplevel - for top level comments; + # all - for all comments. + scope: toplevel + exclude: + - '^ \+.*' + - "^ ANCHOR.*" + importas: + no-unaliased: true + alias: + # Kubernetes + - pkg: k8s.io/api/core/v1 + alias: corev1 + - pkg: k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1 + alias: apiextensionsv1 + - pkg: k8s.io/apimachinery/pkg/apis/meta/v1 + alias: metav1 + - pkg: k8s.io/apimachinery/pkg/api/errors + alias: apierrors + - pkg: k8s.io/apimachinery/pkg/util/errors + alias: kerrors + - pkg: k8s.io/component-base/logs/api/v1 + alias: logsv1 + # Controller Runtime + - pkg: sigs.k8s.io/controller-runtime + alias: ctrl + gofumpt: + extra-rules: true + nolintlint: + allow-unused: false + allow-leading-space: false + require-specific: true + staticcheck: + go: "1.21" + stylecheck: + go: "1.21" + checks: ["all", "-ST1006"] + dot-import-whitelist: + - "github.com/onsi/gomega" + - "github.com/onsi/ginkgo/v2" + gocritic: + enabled-tags: + - diagnostic + - style + - performance + - experimental + - opinionated + revive: + enable-all-rules: true + rules: + - name: dot-imports + disabled: true + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#add-constant + - name: add-constant + severity: warning + disabled: true + arguments: + - maxLitCount: "3" + allowStrs: '""' + allowInts: "0,1,2,3,42,100" + + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#argument-limit + - name: argument-limit + severity: warning + disabled: true + arguments: [3] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#banned-characters + - name: banned-characters + disabled: true + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#cognitive-complexity + - name: cognitive-complexity + severity: warning + disabled: true + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#cyclomatic + - name: cyclomatic + severity: warning + disabled: true + arguments: [10] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#defer + - name: defer + severity: warning + disabled: false + arguments: + - [ "call-chain", "loop", "method-call", "recover", "immediate-recover", "return"] # yamllint disable-line rule:line-length + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#exported + - name: exported + severity: warning + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#file-header + - name: file-header + disabled: true + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#flag-parameter + - name: flag-parameter + disabled: true + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#function-result-limit + - name: function-result-limit + severity: warning + disabled: false + arguments: [3] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#function-length + - name: function-length + severity: warning + disabled: true + arguments: [15, 0] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#imports-blacklist + - name: imports-blacklist + severity: warning + disabled: false + arguments: + - "crypto/md5" + - "crypto/sha1" + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#line-length-limit + - name: line-length-limit + severity: warning + disabled: true + arguments: [120] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#max-public-structs + - name: max-public-structs + severity: warning + disabled: true + arguments: [3] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#string-format + - name: string-format + severity: warning + disabled: false + arguments: + - - "core.WriteError[1].Message" + - "/^([^A-Z]|$)/" + - must not start with a capital letter + - - "fmt.Errorf[0]" + - '/(^|[^\.!?])$/' + - must not end in punctuation + - - panic + - '/^[^\n]*$/' + - must not contain line breaks + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unhandled-error + - name: unhandled-error + severity: warning + disabled: false + arguments: + - "fmt.Printf" + - "fmt.Println" # allow these ones + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#var-naming + - name: var-naming + severity: warning + disabled: false + arguments: + - ["ID"] # AllowList + - ["VM"] # DenyList + # revive changed configuration + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#comment-spacings + - name: comment-spacings + disabled: true + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#use-any + - name: use-any + disabled: true + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#deep-exit + - name: deep-exit + disabled: true + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#receiver-naming + - name: nested-structs + disabled: true + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#struct-tag + - name: struct-tag + disabled: true + unused: + go: "1.21" + usestdlibvars: + # Suggest the use of http.MethodXX. + # Default: true + http-method: true + # Suggest the use of http.StatusXX. + # Default: true + http-status-code: true + # Suggest the use of time.Weekday.String(). + # Default: true + time-weekday: true + # Suggest the use of time.Month.String(). + # Default: false + time-month: true + # Suggest the use of time.Layout. + # Default: false + time-layout: true + # Suggest the use of crypto.Hash.String(). + # Default: false + crypto-hash: true + # Suggest the use of rpc.DefaultXXPath. + # Default: false + default-rpc-path: true + # Suggest the use of os.DevNull. + # Default: false + os-dev-null: true + # Suggest the use of sql.LevelXX.String(). + # Default: false + sql-isolation-level: true + # Suggest the use of tls.SignatureScheme.String(). + # Default: false + tls-signature-scheme: true + # Suggest the use of constant.Kind.String(). + # Default: false + constant-kind: true + # Suggest the use of syslog.Priority. + # Default: false + syslog-priority: true + wrapcheck: + ignoreSigs: + - status.Error( + - .Errorf( + - errors.New( + - errors.Unwrap( + - .Wrap( + - .Wrapf( + - .WithMessage( + - .WithMessagef( + - .WithStack( + - .Complete( +issues: + max-same-issues: 0 + max-issues-per-linter: 0 + # We are disabling default golangci exclusions + # because we want to help reviewers to focus on reviewing the most relevant + # changes in PRs and avoid nitpicking. + exclude-use-default: false + exclude-rules: + - linters: + - wrapcheck + path: _test\.go +run: + timeout: 10m + go: "1.21" + allow-parallel-runners: true + modules-download-mode: vendor + skip-dirs: + - vendor$ + - test/vendor$ diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 00000000..3d8d8a3f --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,16 @@ +project_name: csctl-plugin-openstack + +builds: + - binary: csctl-plugin-openstack + goos: + - linux + - darwin + goarch: + - amd64 + - arm64 + env: + - CGO_ENABLED=0 + ldflags: + - -s -w + - -X 'github.com/SovereignCloudStack/csctl-plugin-openstack/pkg/cmd.Version={{.Version}}' + - -X 'github.com/SovereignCloudStack/csctl-plugin-openstack/pkg/cmd.Commit={{.Commit}}' diff --git a/.lychee.toml b/.lychee.toml new file mode 100644 index 00000000..34b105cf --- /dev/null +++ b/.lychee.toml @@ -0,0 +1,72 @@ +### +### Display +### + +# Show progress +progress = false + + +### +### Runtime +### +# Number of threads to utilize. +# Defaults to number of cores available to the system if omitted. +#threads = 2 + +# Maximum number of allowed redirects +max_redirects = 10 + + +### +### Requests +### +# User agent to send with each request +user_agent = "curl/7.71.1" + +# Website timeout from connect to response finished +timeout = 9 + +# Comma-separated list of accepted status codes for valid links. +# Omit to accept all response types. +#accept = "text/html" + +# Proceed for server connections considered insecure (invalid TLS) +insecure = true + +# Only test links with the given scheme (e.g. https) +# Omit to check links with any scheme +#scheme = "https" + +# Request method +method = "get" + +# Custom request headers +headers = [] + + +### +### Exclusions +### +# Exclude URLs from checking (supports regex) +exclude = [ + "localhost", + "example.com", +] + +include = [] + +# Exclude all private IPs from checking +# Equivalent to setting `exclude_private`, `exclude_link_local`, and `exclude_loopback` to true +exclude_all_private = false + +# Exclude private IP address ranges from checking +exclude_private = false + +# Exclude link-local IP address range from checking +exclude_link_local = false + +# Exclude loopback IP address range from checking +exclude_loopback = true + +# Exclude all mail addresses from checking +exclude_mail = false diff --git a/.yamllint.yaml b/.yamllint.yaml new file mode 100644 index 00000000..8726c794 --- /dev/null +++ b/.yamllint.yaml @@ -0,0 +1,28 @@ +extends: default +rules: + braces: + min-spaces-inside: 0 + max-spaces-inside: 1 + brackets: + min-spaces-inside: 0 + max-spaces-inside: 1 + indentation: + spaces: 2 + indent-sequences: consistent + line-length: disable + new-line-at-end-of-file: enable + truthy: disable + document-start: false + comments: + min-spaces-from-content: 1 + +yaml-files: + - '*.yaml' + - '*.yml' + +ignore: + - '**/vendor/**' + - 'tests/cluster-stacks/**' + - '.cache' + - _artifacts + - dist diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..4f35899d --- /dev/null +++ b/Makefile @@ -0,0 +1,185 @@ +# Copyright 2023 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +.DEFAULT_GOAL:=help + +# variables +TAG ?= dev +ARCH ?= amd64 +IMAGE_PREFIX ?= ghcr.io/sovereigncloudstack +BUILDER_IMAGE = $(IMAGE_PREFIX)/builder +BUILDER_IMAGE_VERSION = $(shell cat .builder-image-version.txt) +Version := $(shell git describe --tags --always --dirty) +Commit := $(shell git rev-parse HEAD) +LDFLAGS := -X github.com/SovereignCloudStack/csctl-plugin-openstack/pkg/cmd.Version=$(Version) -X github.com/SovereignCloudStack/csctl-plugin-openstack/pkg/cmd.Commit=$(Commit) + +# Certain aspects of the build are done in containers for consistency (e.g. protobuf generation) +# If you have the correct tools installed and you want to speed up development you can run +# make BUILD_IN_CONTAINER=false target +# or you can override this with an environment variable +BUILD_IN_CONTAINER ?= true + +.PHONY: help +help: ## Display this help. + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + +# Directories +ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) +BIN_DIR := bin +TOOLS_DIR := hack/tools +TOOLS_BIN_DIR := $(TOOLS_DIR)/$(BIN_DIR) +export PATH := $(abspath $(TOOLS_BIN_DIR)):$(PATH) +export GOBIN := $(abspath $(TOOLS_BIN_DIR)) + +##@ Clean +######### +# Clean # +######### + +.PHONY: clean +clean: ## cleans the csctl-plugin-openstack binary + @if [ -f csctl-plugin-openstack ]; then rm csctl-plugin-openstack; fi + + +##@ Common +########## +# Common # +########## +.PHONY: build +build: # build the csctl-plugin-openstack binary + go build -ldflags "$(LDFLAGS)" -o csctl-plugin-openstack main.go + +.PHONY: lint-golang +lint-golang: ## Lint Golang codebase +ifeq ($(BUILD_IN_CONTAINER),true) + docker run --rm -t -i \ + -v $(shell go env GOPATH)/pkg:/go/pkg$(MOUNT_FLAGS) \ + -v $(shell pwd):/src$(MOUNT_FLAGS) \ + $(BUILDER_IMAGE):$(BUILDER_IMAGE_VERSION) $@; +else + go version + golangci-lint version + golangci-lint run -v +endif + +.PHONY: lint-golang-ci +lint-golang-ci: +ifeq ($(BUILD_IN_CONTAINER),true) + docker run --rm -t -i \ + -v $(shell go env GOPATH)/pkg:/go/pkg$(MOUNT_FLAGS) \ + -v $(shell pwd):/src$(MOUNT_FLAGS) \ + $(BUILDER_IMAGE):$(BUILDER_IMAGE_VERSION) $@; +else + go version + golangci-lint version + golangci-lint run -v --out-format=github-actions +endif + +.PHONY: lint-yaml +lint-yaml: ## Lint YAML files +ifeq ($(BUILD_IN_CONTAINER),true) + docker run --rm -t -i \ + -v $(shell go env GOPATH)/pkg:/go/pkg$(MOUNT_FLAGS) \ + -v $(shell pwd):/src$(MOUNT_FLAGS) \ + $(BUILDER_IMAGE):$(BUILDER_IMAGE_VERSION) $@; +else + yamllint --version + yamllint -c .yamllint.yaml --strict . +endif + +.PHONY: lint-yaml-ci +lint-yaml-ci: +ifeq ($(BUILD_IN_CONTAINER),true) + docker run --rm -t -i \ + -v $(shell go env GOPATH)/pkg:/go/pkg$(MOUNT_FLAGS) \ + -v $(shell pwd):/src$(MOUNT_FLAGS) \ + $(BUILDER_IMAGE):$(BUILDER_IMAGE_VERSION) $@; +else + yamllint --version + yamllint -c .yamllint.yaml . --format github +endif + +lint-links: ## Link Checker +ifeq ($(BUILD_IN_CONTAINER),true) + docker run --rm -t -i \ + -v $(shell pwd):/src$(MOUNT_FLAGS) \ + $(BUILDER_IMAGE):$(BUILDER_IMAGE_VERSION) $@; +else + lychee --version + lychee --config .lychee.toml ./*.md ./docs/**/*.md +endif + +.PHONY: format-golang +format-golang: ## Format the Go codebase and run auto-fixers if supported by the linter. +ifeq ($(BUILD_IN_CONTAINER),true) + docker run --rm -t -i \ + -v $(shell go env GOPATH)/pkg:/go/pkg$(MOUNT_FLAGS) \ + -v $(shell pwd):/src$(MOUNT_FLAGS) \ + $(BUILDER_IMAGE):$(BUILDER_IMAGE_VERSION) $@; +else + go version + golangci-lint version + golangci-lint run -v --fix +endif + +.PHONY: generate-boilerplate +generate-boilerplate: ## Generates missing boilerplates + ./hack/ensure-boilerplate.sh + +# support go modules +generate-modules: ## Generates missing go modules +ifeq ($(BUILD_IN_CONTAINER),true) + docker run --rm -t -i \ + -v $(shell go env GOPATH)/pkg:/go/pkg$(MOUNT_FLAGS) \ + -v $(shell pwd):/src$(MOUNT_FLAGS) \ + $(BUILDER_IMAGE):$(BUILDER_IMAGE_VERSION) $@; +else + ./hack/golang-modules-update.sh +endif + +generate-modules-ci: generate-modules + @if ! (git diff --exit-code ); then \ + echo "\nChanges found in generated files"; \ + exit 1; \ + fi + +.PHONY: verify-boilerplate +verify-boilerplate: + ./hack/verify-boilerplate.sh + +.PHONY: verify-shellcheck +verify-shellcheck: ## Verify shell files + ./hack/verify-shellcheck.sh + +.PHONY: generate +generate: generate-boilerplate generate-modules + +ALL_VERIFY_CHECKS = boilerplate shellcheck +.PHONY: verify +verify: generate lint $(addprefix verify-,$(ALL_VERIFY_CHECKS)) ## Verify all + +.PHONY: modules +modules: generate-modules ## Update go.mod & go.sum + +.PHONY: boilerplate +boilerplate: generate-boilerplate ## Ensure that your files have a boilerplate header + +# .PHONY: test +# test: test-unit ## Runs all unit and integration tests. + +.PHONY: lint +lint: lint-golang lint-yaml lint-links ## Lint Codebase + +.PHONY: format +format: format-golang ## Format Codebase diff --git a/go.mod b/go.mod new file mode 100644 index 00000000..e7796f98 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/SovereignCloudStack/csctl-plugin-openstack + +go 1.21 diff --git a/hack/boilerplate/boilerplate.Dockerfile.txt b/hack/boilerplate/boilerplate.Dockerfile.txt new file mode 100644 index 00000000..384f325a --- /dev/null +++ b/hack/boilerplate/boilerplate.Dockerfile.txt @@ -0,0 +1,14 @@ +# Copyright YEAR The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/hack/boilerplate/boilerplate.Makefile.txt b/hack/boilerplate/boilerplate.Makefile.txt new file mode 100644 index 00000000..384f325a --- /dev/null +++ b/hack/boilerplate/boilerplate.Makefile.txt @@ -0,0 +1,14 @@ +# Copyright YEAR The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/hack/boilerplate/boilerplate.bzl.txt b/hack/boilerplate/boilerplate.bzl.txt new file mode 100644 index 00000000..384f325a --- /dev/null +++ b/hack/boilerplate/boilerplate.bzl.txt @@ -0,0 +1,14 @@ +# Copyright YEAR The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/hack/boilerplate/boilerplate.generatego.txt b/hack/boilerplate/boilerplate.generatego.txt new file mode 100644 index 00000000..b7c650da --- /dev/null +++ b/hack/boilerplate/boilerplate.generatego.txt @@ -0,0 +1,16 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + diff --git a/hack/boilerplate/boilerplate.go.txt b/hack/boilerplate/boilerplate.go.txt new file mode 100644 index 00000000..59e740c1 --- /dev/null +++ b/hack/boilerplate/boilerplate.go.txt @@ -0,0 +1,16 @@ +/* +Copyright YEAR The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + diff --git a/hack/boilerplate/boilerplate.py b/hack/boilerplate/boilerplate.py new file mode 100755 index 00000000..bfd7ece6 --- /dev/null +++ b/hack/boilerplate/boilerplate.py @@ -0,0 +1,322 @@ +#!/usr/bin/env python + +# Copyright 2015 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import print_function + +import argparse +import datetime +import difflib +import glob +import os +import re +import sys +import json + +parser = argparse.ArgumentParser() +parser.add_argument( + "filenames", + help="list of files to check, all files if unspecified", + nargs='*') + +# Rootdir defaults to the directory **above** the repo-infra dir. +rootdir = os.path.dirname(__file__) + "/../../" +rootdir = os.path.abspath(rootdir) +parser.add_argument( + "--rootdir", default=rootdir, help="root directory to examine") + +default_boilerplate_dir = os.path.join(rootdir, "repo-infra/verify/boilerplate") +parser.add_argument( + "--boilerplate-dir", default=default_boilerplate_dir) + +parser.add_argument( + "-v", "--verbose", + help="give verbose output regarding why a file does not pass", + action="store_true") + +parser.add_argument( + "--ensure", + help="ensure all files which should have appropriate licence headers have them prepended", + action="store_true") + +args = parser.parse_args() + +verbose_out = sys.stderr if args.verbose else open("/dev/null", "w") + +default_skipped_dirs = ['Godeps', '.git', 'vendor', 'third_party', '_gopath', '_output'] + +# list all the files that contain 'DO NOT EDIT', but are not generated +default_skipped_not_generated = [] + + +def get_refs(): + refs = {} + + for path in glob.glob(os.path.join(args.boilerplate_dir, "boilerplate.*.txt")): + extension = os.path.basename(path).split(".")[1] + + ref_file = open(path, 'r') + ref = ref_file.read().splitlines() + ref_file.close() + refs[extension] = ref + + return refs + + +def is_generated_file(filename, data, regexs, files_to_skip): + for d in files_to_skip: + if d in filename: + return False + + p = regexs["generated"] + return p.search(data) + + +def match_and_delete(content, re): + match = re.search(content) + if match is None: + return content, None + return re.sub("", content, 1), match.group() + + +def replace_specials(content, extension, regexs): + # remove build tags from the top of Go files + if extension == "go" or extension == "generatego": + re = regexs["go_build_constraints"] + return match_and_delete(content, re) + + # remove shebang from the top of shell files + if extension == "sh": + re = regexs["shebang"] + return match_and_delete(content, re) + + return content, None + + +def file_passes(filename, refs, regexs, not_generated_files_to_skip): + try: + f = open(filename, 'r') + except Exception as exc: + print("Unable to open %s: %s" % (filename, exc), file=verbose_out) + return False + + data = f.read() + f.close() + + ref, extension, generated = analyze_file( + filename, data, refs, regexs, not_generated_files_to_skip) + + return file_content_passes(data, filename, ref, extension, generated, regexs) + + +def file_content_passes(data, filename, ref, extension, generated, regexs): + if ref is None: + return True + + data, _ = replace_specials(data, extension, regexs) + + data = data.splitlines() + + # if our test file is smaller than the reference it surely fails! + if len(ref) > len(data): + print('File %s smaller than reference (%d < %d)' % + (filename, len(data), len(ref)), + file=verbose_out) + return False + + # trim our file to the same number of lines as the reference file + data = data[:len(ref)] + + p = regexs["year"] + for d in data: + if p.search(d): + if generated: + print('File %s has the YEAR field, but it should not be in generated file' % filename, file=verbose_out) + else: + print('File %s has the YEAR field, but missing the year of date' % filename, file=verbose_out) + return False + + if not generated: + # Replace all occurrences of the regex "2014|2015|2016|2017|2018" with "YEAR" + p = regexs["date"] + for i, d in enumerate(data): + (data[i], found) = p.subn('YEAR', d) + if found != 0: + break + + # if we don't match the reference at this point, fail + if ref != data: + print("Header in %s does not match reference, diff:" % filename, file=verbose_out) + if args.verbose: + print(file=verbose_out) + for line in difflib.unified_diff(ref, data, 'reference', filename, lineterm=''): + print(line, file=verbose_out) + print(file=verbose_out) + return False + + return True + + +def file_extension(filename): + return os.path.splitext(filename)[1].split(".")[-1].lower() + + +def read_config_file(conf_path): + try: + with open(conf_path) as json_data_file: + return json.load(json_data_file) + except ValueError: + raise + except: + return {'dirs_to_skip': default_skipped_dirs, 'not_generated_files_to_skip': default_skipped_not_generated} + + +def normalize_files(files, dirs_to_skip): + newfiles = [] + for pathname in files: + if any(x in pathname for x in dirs_to_skip): + continue + newfiles.append(pathname) + for i, pathname in enumerate(newfiles): + if not os.path.isabs(pathname): + newfiles[i] = os.path.join(args.rootdir, pathname) + return newfiles + + +def get_files(extensions, dirs_to_skip): + files = [] + if len(args.filenames) > 0: + files = args.filenames + else: + for root, dirs, walkfiles in os.walk(args.rootdir): + # don't visit certain dirs. This is just a performance improvement + # as we would prune these later in normalize_files(). But doing it + # cuts down the amount of filesystem walking we do and cuts down + # the size of the file list + for d in dirs_to_skip: + if d in dirs: + dirs.remove(d) + + for name in walkfiles: + pathname = os.path.join(root, name) + files.append(pathname) + + files = normalize_files(files, dirs_to_skip) + outfiles = [] + for pathname in files: + basename = os.path.basename(pathname) + extension = file_extension(pathname) + if extension in extensions or basename in extensions: + outfiles.append(pathname) + return outfiles + + +def analyze_file(file_name, file_content, refs, regexs, not_generated_files_to_skip): + # determine if the file is automatically generated + generated = is_generated_file( + file_name, file_content, regexs, not_generated_files_to_skip) + + base_name = os.path.basename(file_name) + if generated: + extension = "generatego" + else: + extension = file_extension(file_name) + + if extension != "": + ref = refs[extension] + else: + ref = refs.get(base_name, None) + + return ref, extension, generated + + +def ensure_boilerplate_file(file_name, refs, regexs, not_generated_files_to_skip): + with open(file_name, mode='r+') as f: + file_content = f.read() + + ref, extension, generated = analyze_file( + file_name, file_content, refs, regexs, not_generated_files_to_skip) + + # licence header + licence_header = os.linesep.join(ref) + + # content without shebang and such + content_without_specials, special_header = replace_specials( + file_content, extension, regexs) + + # new content, to be written to the file + new_content = '' + + # shebang and such + if special_header is not None: + new_content += special_header + + # licence header + current_year = str(datetime.datetime.now().year) + year_replacer = regexs['year'] + new_content += year_replacer.sub(current_year, licence_header, 1) + + # actual content + new_content += os.linesep + content_without_specials + + f.seek(0) + f.write(new_content) + + +def get_dates(): + years = datetime.datetime.now().year + return '(%s)' % '|'.join((str(year) for year in range(2014, years+1))) + + +def get_regexs(): + regexs = {} + # Search for "YEAR" which exists in the boilerplate, but shouldn't in the real thing + regexs["year"] = re.compile('YEAR') + # get_dates return 2014, 2015, 2016, 2017, or 2018 until the current year as a regex like: "(2014|2015|2016|2017|2018)"; + # company holder names can be anything + regexs["date"] = re.compile(get_dates()) + # strip // +build \n\n build constraints + regexs["go_build_constraints"] = re.compile( + r"^(// \+build.*\n)+\n", re.MULTILINE) + # strip #!.* from shell scripts + regexs["shebang"] = re.compile(r"^(#!.*\n)\n*", re.MULTILINE) + # Search for generated files + regexs["generated"] = re.compile('DO NOT EDIT') + return regexs + + +def main(): + config_file_path = os.path.join(args.rootdir, ".boilerplate.json") + config = read_config_file(config_file_path) + + regexs = get_regexs() + refs = get_refs() + filenames = get_files(refs.keys(), config.get('dirs_to_skip')) + not_generated_files_to_skip = config.get('not_generated_files_to_skip', []) + + for filename in filenames: + if not file_passes(filename, refs, regexs, not_generated_files_to_skip): + if args.ensure: + print("adding boilerplate header to %s" % filename) + ensure_boilerplate_file( + filename, refs, regexs, not_generated_files_to_skip) + else: + print(filename, file=sys.stdout) + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/hack/boilerplate/boilerplate.py.txt b/hack/boilerplate/boilerplate.py.txt new file mode 100644 index 00000000..a2e72e59 --- /dev/null +++ b/hack/boilerplate/boilerplate.py.txt @@ -0,0 +1,16 @@ +#!/usr/bin/env python + +# Copyright YEAR The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/hack/boilerplate/boilerplate.sh.txt b/hack/boilerplate/boilerplate.sh.txt new file mode 100644 index 00000000..384f325a --- /dev/null +++ b/hack/boilerplate/boilerplate.sh.txt @@ -0,0 +1,14 @@ +# Copyright YEAR The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/hack/boilerplate/boilerplate_test.py b/hack/boilerplate/boilerplate_test.py new file mode 100644 index 00000000..52579355 --- /dev/null +++ b/hack/boilerplate/boilerplate_test.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python + +# Copyright 2016 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import print_function +import boilerplate +import unittest +import io +import os +import sys +import tempfile +import re +from contextlib import contextmanager + +base_dir = os.getcwd() + + +class DefaultArgs(object): + def __init__(self): + self.filenames = [] + self.rootdir = "." + self.boilerplate_dir = base_dir + self.verbose = True + self.ensure = False + + +class TestBoilerplate(unittest.TestCase): + """ + Note: run this test from the inside the boilerplate directory. + + $ python -m unittest boilerplate_test + """ + + def setUp(self): + os.chdir(base_dir) + boilerplate.args = DefaultArgs() + + def test_boilerplate(self): + os.chdir("testdata/default/") + + # capture stdout + old_stdout = sys.stdout + sys.stdout = io.StringIO() + + ret = boilerplate.main() + + output = sorted(sys.stdout.getvalue().split()) + + sys.stdout = old_stdout + + self.assertCountEqual( + output, ['././fail.go', '././fail.py', '././fail.sh']) + + def test_read_config(self): + config_file = "./testdata/with_config/.boilerplate.json" + config = boilerplate.read_config_file(config_file) + self.assertCountEqual(config.get('dirs_to_skip'), [ + 'dir_to_skip', 'dont_want_this', 'not_interested', '.']) + self.assertCountEqual(config.get('not_generated_files_to_skip'), [ + 'alice skips a file', 'bob skips another file']) + + def test_read_nonexistent_config(self): + config_file = '/nonexistent' + config = boilerplate.read_config_file(config_file) + self.assertCountEqual(config['dirs_to_skip'], + boilerplate.default_skipped_dirs) + self.assertCountEqual(config['not_generated_files_to_skip'], + boilerplate.default_skipped_not_generated) + + def test_read_malformed_config(self): + config_file = './testdata/with_config/.boilerplate.bad.json' + with self.assertRaises(Exception): + boilerplate.read_config_file(config_file) + + def test_read_config_called_with_correct_path(self): + boilerplate.args.rootdir = "/tmp/some/path" + with function_mocker('read_config_file', boilerplate, return_value={}) as mock_args: + boilerplate.main() + self.assertEqual(len(mock_args), 1) + self.assertEqual( + mock_args[0][0], "/tmp/some/path/.boilerplate.json") + + def test_get_files_with_skipping_dirs(self): + refs = boilerplate.get_refs() + skip_dirs = ['.'] + files = boilerplate.get_files(refs, skip_dirs) + + self.assertEqual(files, []) + + def test_get_files_with_skipping_not_generated_files(self): + refs = boilerplate.get_refs() + regexes = boilerplate.get_regexs() + files_to_skip = ['boilerplate.py'] + filename = 'boilerplate.py' + + passes = boilerplate.file_passes( + filename, refs, regexes, files_to_skip) + + self.assertEqual(passes, True) + + def test_ignore_when_no_valid_boilerplate_template(self): + with tempfile.NamedTemporaryFile() as temp_file_to_check: + passes = boilerplate.file_passes( + temp_file_to_check.name, boilerplate.get_refs(), boilerplate.get_regexs(), []) + self.assertEqual(passes, True) + + def test_add_boilerplate_to_file(self): + with tmp_copy("./testdata/default/fail.sh", suffix='.sh') as tmp_file_name: + boilerplate.ensure_boilerplate_file( + tmp_file_name, boilerplate.get_refs(), boilerplate.get_regexs(), [] + ) + + passes = boilerplate.file_passes( + tmp_file_name, boilerplate.get_refs(), boilerplate.get_regexs(), []) + self.assertEqual(passes, True) + + with open(tmp_file_name) as x: + first_line = x.read().splitlines()[0] + self.assertEqual(first_line, '#!/usr/bin/env bash') + + def test_replace_specials(self): + extension = "sh" + regexs = boilerplate.get_regexs() + + original_content = "\n".join([ + "#!/usr/bin/env bash", + "", + "something something", + "#!/usr/bin/env bash", + ]) + expected_content = "\n".join([ + "something something", + "#!/usr/bin/env bash", + ]) + expected_match = "\n".join([ + "#!/usr/bin/env bash", + "\n", + ]) + + actual_content, actual_match = boilerplate.replace_specials( + original_content, extension, regexs + ) + + self.assertEqual(actual_content, expected_content) + self.assertEqual(actual_match, expected_match) + + def test_ensure_command_line_flag(self): + os.chdir("./testdata/default/") + boilerplate.args.ensure = True + + with function_mocker('ensure_boilerplate_file', boilerplate) as mock_args: + boilerplate.main() + changed_files = list(map(lambda x: x[0], mock_args)) + + self.assertCountEqual(changed_files, [ + "././fail.sh", + "././fail.py", + "././fail.go", + ]) + + +@contextmanager +def tmp_copy(file_org, suffix=None): + file_copy_fd, file_copy = tempfile.mkstemp(suffix) + + with open(file_org) as org: + os.write(file_copy_fd, bytes(org.read(),'utf-8')) + os.close(file_copy_fd) + + yield file_copy + + os.unlink(file_copy) + + +@contextmanager +def function_mocker(function_name, original_holder, return_value=None): + # save original function implementation + original_implementation = getattr(original_holder, function_name) + + # keep track of the args + mock_call_args = [] + + # mock the function + def the_mock(*args): + mock_call_args.append(args) + if return_value is not None: + return return_value + + # use the mock in place of the original implementation + setattr(original_holder, function_name, the_mock) + + # run + yield mock_call_args + + # reset the original implementation + setattr(original_holder, function_name, original_implementation) diff --git a/hack/boilerplate/testdata/default/fail.go b/hack/boilerplate/testdata/default/fail.go new file mode 100644 index 00000000..f822e023 --- /dev/null +++ b/hack/boilerplate/testdata/default/fail.go @@ -0,0 +1,36 @@ +/* +Copyright 2023 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +Copyright 2014 The Kubernetes Authors. + +fail + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package test contains test boilerplate. +package test diff --git a/hack/boilerplate/testdata/default/fail.py b/hack/boilerplate/testdata/default/fail.py new file mode 100644 index 00000000..3c7033dd --- /dev/null +++ b/hack/boilerplate/testdata/default/fail.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python + +# Copyright 2023 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +#!/usr/bin/env python + +# Copyright 2015 The Kubernetes Authors. +# +# failed +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/hack/boilerplate/testdata/default/fail.sh b/hack/boilerplate/testdata/default/fail.sh new file mode 100644 index 00000000..d3ddb5b4 --- /dev/null +++ b/hack/boilerplate/testdata/default/fail.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +# Copyright 2022 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +## invalid header + +exit_code=42 +exit $exit_code \ No newline at end of file diff --git a/hack/boilerplate/testdata/default/pass.go b/hack/boilerplate/testdata/default/pass.go new file mode 100644 index 00000000..7508448a --- /dev/null +++ b/hack/boilerplate/testdata/default/pass.go @@ -0,0 +1,17 @@ +/* +Copyright 2014 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package test diff --git a/hack/boilerplate/testdata/default/pass.py b/hack/boilerplate/testdata/default/pass.py new file mode 100644 index 00000000..5b7ce29a --- /dev/null +++ b/hack/boilerplate/testdata/default/pass.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python + +# Copyright 2015 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +True diff --git a/hack/boilerplate/testdata/with_config/.boilerplate.bad.json b/hack/boilerplate/testdata/with_config/.boilerplate.bad.json new file mode 100644 index 00000000..9314feb2 --- /dev/null +++ b/hack/boilerplate/testdata/with_config/.boilerplate.bad.json @@ -0,0 +1,3 @@ +{ + "dirs_to_skip": [ +} \ No newline at end of file diff --git a/hack/boilerplate/testdata/with_config/.boilerplate.json b/hack/boilerplate/testdata/with_config/.boilerplate.json new file mode 100644 index 00000000..8a497169 --- /dev/null +++ b/hack/boilerplate/testdata/with_config/.boilerplate.json @@ -0,0 +1,12 @@ +{ + "dirs_to_skip": [ + "dir_to_skip", + "dont_want_this", + "not_interested", + "." + ], + "not_generated_files_to_skip": [ + "alice skips a file", + "bob skips another file" + ] +} \ No newline at end of file diff --git a/hack/boilerplate/testdata/with_config/fail.go b/hack/boilerplate/testdata/with_config/fail.go new file mode 100644 index 00000000..e6faf963 --- /dev/null +++ b/hack/boilerplate/testdata/with_config/fail.go @@ -0,0 +1,31 @@ +/* +Copyright 2023 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +Copyright 2014 The Kubernetes Authors. +fail +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package test diff --git a/hack/boilerplate/testdata/with_config/fail.py b/hack/boilerplate/testdata/with_config/fail.py new file mode 100644 index 00000000..d4193660 --- /dev/null +++ b/hack/boilerplate/testdata/with_config/fail.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python + +# Copyright 2023 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +#!/usr/bin/env python + +# Copyright 2015 The Kubernetes Authors. +# +# failed +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. \ No newline at end of file diff --git a/hack/boilerplate/testdata/with_config/fail.sh b/hack/boilerplate/testdata/with_config/fail.sh new file mode 100644 index 00000000..fb324470 --- /dev/null +++ b/hack/boilerplate/testdata/with_config/fail.sh @@ -0,0 +1,30 @@ +# Copyright 2023 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Copyright YEAR The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +exit_code=42 +exit $exit_code diff --git a/hack/boilerplate/testdata/with_config/pass.go b/hack/boilerplate/testdata/with_config/pass.go new file mode 100644 index 00000000..7508448a --- /dev/null +++ b/hack/boilerplate/testdata/with_config/pass.go @@ -0,0 +1,17 @@ +/* +Copyright 2014 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package test diff --git a/hack/boilerplate/testdata/with_config/pass.py b/hack/boilerplate/testdata/with_config/pass.py new file mode 100644 index 00000000..21d005d8 --- /dev/null +++ b/hack/boilerplate/testdata/with_config/pass.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python + +# Copyright 2015 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +True \ No newline at end of file diff --git a/hack/ensure-boilerplate.sh b/hack/ensure-boilerplate.sh new file mode 100755 index 00000000..e760919b --- /dev/null +++ b/hack/ensure-boilerplate.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash + +# Copyright 2014 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -o errexit +set -o nounset +set -o pipefail + +if [ "$(uname)" = 'Darwin' ]; then + readlinkf(){ perl -MCwd -e 'print Cwd::abs_path shift' "$1";} +else + readlinkf(){ readlink -f "$1"; } +fi + +# shellcheck disable=SC2128 +SCRIPT_DIR="$(cd "$(dirname "$(readlinkf "$BASH_SOURCE")")" ; pwd)" + +# We assume the link to the script ( {ensure,verify}-boilerplate.sh ) to be +# in a directory 2 levels down from the repo root, e.g. in +# /repo-infra/verify/verify-boilerplate.sh +# Alternatively, you can set the project root by setting the variable +# `REPO_ROOT`. +# +# shellcheck disable=SC2128 +: "${REPO_ROOT:="$(cd "${SCRIPT_DIR}/.." ; pwd)"}" + +boilerDir="${SCRIPT_DIR}/boilerplate/" +boiler="${boilerDir}/boilerplate.py" + +verify() { + # shellcheck disable=SC2207 + files_need_boilerplate=( + $( "$boiler" --rootdir="$REPO_ROOT" --boilerplate-dir="$boilerDir" "$@") + ) + + # Run boilerplate check + if [[ ${#files_need_boilerplate[@]} -gt 0 ]]; then + for file in "${files_need_boilerplate[@]}"; do + echo "Boilerplate header is wrong for: ${file}" >&2 + done + + return 1 + fi +} + +ensure() { + "$boiler" --rootdir="$REPO_ROOT" --boilerplate-dir="$boilerDir" --ensure "$@" +} + +case "$0" in + */ensure-boilerplate.sh) + ensure "$@" + ;; + */verify-boilerplate.sh) + verify "$@" + ;; + *) + { + echo "unknown command '$0'" + echo "" + echo "Call the script as either 'verify-boilerplate.sh' or 'ensure-boilerplate.sh'" + } >&2 + + exit 1 + ;; +esac \ No newline at end of file diff --git a/hack/golang-modules-update.sh b/hack/golang-modules-update.sh new file mode 100755 index 00000000..2a9f1792 --- /dev/null +++ b/hack/golang-modules-update.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash + +# Copyright 2023 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -o errexit +set -o nounset +set -o pipefail + +REPO_ROOT=$(realpath $(dirname "${BASH_SOURCE[0]}")/..) +cd "${REPO_ROOT}" || exit 1 + +DIRS="./" +for DIR in ${DIRS}; do + cd ${REPO_ROOT}/${DIR} && go mod download + cd ${REPO_ROOT}/${DIR} && go mod verify + cd ${REPO_ROOT}/${DIR} && go mod tidy + cd ${REPO_ROOT}/${DIR} && go mod vendor +done + diff --git a/hack/lib/utils.sh b/hack/lib/utils.sh new file mode 100644 index 00000000..2e48b25a --- /dev/null +++ b/hack/lib/utils.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# Copyright 2022 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# get_root_path returns the root path of the project source tree +get_root_path() { + git rev-parse --show-toplevel +} + +# cd_root_path cds to the root path of the project source tree +cd_root_path() { + cd "$(get_root_path)" || exit +} diff --git a/hack/semver-upgrade.sh b/hack/semver-upgrade.sh new file mode 100755 index 00000000..e9e8b23e --- /dev/null +++ b/hack/semver-upgrade.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# Copyright 2023 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -o errexit +set -o nounset +set -o pipefail +set -x + +semver_upgrade() { +IFS=. read -r version minor patch <&2 + done + + exit 1 +fi diff --git a/hack/verify-shellcheck.sh b/hack/verify-shellcheck.sh new file mode 100755 index 00000000..7487fd7d --- /dev/null +++ b/hack/verify-shellcheck.sh @@ -0,0 +1,101 @@ +#!/usr/bin/env bash + +# Copyright 2022 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -o errexit +set -o nounset +set -o pipefail +VERSION="v0.8.0" + +# disabled lints +disabled=( + # this lint disallows non-constant source, which we use extensively without + # any known bugs + 1090 + # this lint prefers command -v to which, they are not the same + 2230 + + 1091 +) + +OS="unknown" +if [[ "${OSTYPE}" == "linux"* ]]; then + OS="linux" +elif [[ "${OSTYPE}" == "darwin"* ]]; then + OS="darwin" +fi + +# comma separate for passing to shellcheck +join_by() { + local IFS="$1"; + shift; + echo "$*"; +} +# shellcheck source=./hack/utils.sh +source "$(dirname "$0")/lib/utils.sh" +ROOT_PATH=$(get_root_path) + +# create a temporary directory +TMP_DIR=$(mktemp -d) +OUT="${TMP_DIR}/out.log" + + +# cleanup on exit +cleanup() { + ret=0 + if [[ -s "${OUT}" ]]; then + echo "Found errors:" + cat "${OUT}" + echo + echo 'Please review the above warnings. You can test via "./hack/verify-shellcheck.sh"' + echo 'If the above warnings do not make sense, you can exempt this warning with a comment' + echo ' (if your reviewer is okay with it).' + echo 'In general please prefer to fix the error, we have already disabled specific lints.' + echo 'See: https://github.com/koalaman/shellcheck/wiki/Ignore#ignoring-one-specific-instance-in-a-file' + echo + ret=1 + else + echo 'Congratulations! All shell files are passing lint :-)' + fi + echo "Cleaning up..." + rm -rf "${TMP_DIR}" + exit ${ret} +} +trap cleanup EXIT + +SHELLCHECK_DISABLED="$(join_by , "${disabled[@]}")" +readonly SHELLCHECK_DISABLED + +SHELLCHECK="./$(dirname "$0")/tools/bin/shellcheck/${VERSION}/shellcheck" + +if [ ! -f "$SHELLCHECK" ]; then + # install buildifier + cd "${TMP_DIR}" || exit + DOWNLOAD_FILE="shellcheck-${VERSION}.${OS}.x86_64.tar.xz" + curl -L "https://github.com/koalaman/shellcheck/releases/download/${VERSION}/${DOWNLOAD_FILE}" -o "${TMP_DIR}/shellcheck.tar.xz" + tar xf "${TMP_DIR}/shellcheck.tar.xz" + cd "${ROOT_PATH}" + mkdir -p "$(dirname "$0")/tools/bin/shellcheck/${VERSION}" + mv "${TMP_DIR}/shellcheck-${VERSION}/shellcheck" "$SHELLCHECK" +fi + +echo "Running shellcheck..." +cd "${ROOT_PATH}" || exit +IGNORE_FILES=$(find . -name "*.sh" | grep "third_party\|tilt_modules|node_modules|vendor") +echo "Ignoring shellcheck on ${IGNORE_FILES}" +FILES=$(find . -name "*.sh" -not -path "./tilt_modules/*" -not -path "./vendor/*" -not -path "./hack/*" -not -path "./templates/*" -not -path "./hack/version.sh" -not -path "./hack/tools/vendor/*" -not -path "*third_party*" -not -path "*node_modules*") +while read -r file; do + "$SHELLCHECK" -x "--exclude=${SHELLCHECK_DISABLED}" "--color=auto" "$file" >> "${OUT}" 2>&1 +done <<< "$FILES"