From 76695a48437570acf50ff8696c1e5fbc72c41133 Mon Sep 17 00:00:00 2001 From: Aleksander Zaruczewski Date: Tue, 6 Feb 2024 14:11:09 +0200 Subject: [PATCH] chore: CI, linting, and community docs --- .github/CODEOWNERS | 1 + .github/ISSUE_TEMPLATE/01_question.md | 14 ++ .github/ISSUE_TEMPLATE/02_bug.md | 18 +++ .github/ISSUE_TEMPLATE/03_feature.md | 18 +++ .github/ISSUE_TEMPLATE/config.yml | 5 + .github/PULL_REQUEST_TEMPLATE.md | 15 ++ .github/workflows/codeql-analysis.yml | 44 ++++++ .github/workflows/lint.yml | 39 +++++ .github/workflows/test.yml | 34 +++++ .github/workflows/trunk-upgrade.yml | 28 ++++ .gitignore | 210 ++++++++++++++++++++++++++ .golangci.yaml | 55 +++++++ .trunk/.gitignore | 9 ++ .trunk/configs/.markdownlint.yaml | 10 ++ .trunk/configs/.yamllint.yaml | 10 ++ .trunk/trunk.yaml | 37 +++++ CODE_OF_CONDUCT.md | 128 ++++++++++++++++ CONTRIBUTING.md | 94 ++++++++++++ LICENSE | 201 ++++++++++++++++++++++++ Makefile | 16 -- README.md | 99 ++++-------- SECURITY.md | 32 ++++ Taskfile.yml | 21 +++ client.go | 16 +- client_generated.go | 68 ++++----- client_test.go | 3 + commitlint.config.js | 4 + error.go | 4 + error_test.go | 1 + generator/main.go | 56 ++++++- generator/models.go | 58 +++++-- go.mod | 7 +- go.sum | 6 +- option.go | 2 + retry.go | 11 +- 35 files changed, 1222 insertions(+), 152 deletions(-) create mode 100644 .github/CODEOWNERS create mode 100644 .github/ISSUE_TEMPLATE/01_question.md create mode 100644 .github/ISSUE_TEMPLATE/02_bug.md create mode 100644 .github/ISSUE_TEMPLATE/03_feature.md create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/codeql-analysis.yml create mode 100644 .github/workflows/lint.yml create mode 100644 .github/workflows/test.yml create mode 100644 .github/workflows/trunk-upgrade.yml create mode 100644 .golangci.yaml create mode 100644 .trunk/.gitignore create mode 100644 .trunk/configs/.markdownlint.yaml create mode 100644 .trunk/configs/.yamllint.yaml create mode 100644 .trunk/trunk.yaml create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE delete mode 100644 Makefile create mode 100644 SECURITY.md create mode 100644 Taskfile.yml create mode 100644 commitlint.config.js diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..34f5367 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @aiven/team-space-invaders diff --git a/.github/ISSUE_TEMPLATE/01_question.md b/.github/ISSUE_TEMPLATE/01_question.md new file mode 100644 index 0000000..1ad1483 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/01_question.md @@ -0,0 +1,14 @@ +--- +name: ❓ Ask a question +about: Got stuck or missing something from the docs? Ask away! +--- + +# Fill the question form below and remove this heading + +## What can we help you with? + + + +## Where would you expect to find this information? + + diff --git a/.github/ISSUE_TEMPLATE/02_bug.md b/.github/ISSUE_TEMPLATE/02_bug.md new file mode 100644 index 0000000..34c5987 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/02_bug.md @@ -0,0 +1,18 @@ +--- +name: 🐜 Report a bug +about: Spotted a problem? Let us know +--- + +# Fill the bug report form below and remove this heading + +## What happened? + + + +## What did you expect to happen? + + + +## What else do we need to know? + + diff --git a/.github/ISSUE_TEMPLATE/03_feature.md b/.github/ISSUE_TEMPLATE/03_feature.md new file mode 100644 index 0000000..519c0c0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/03_feature.md @@ -0,0 +1,18 @@ +--- +name: 💡 Feature suggestion +about: What would make this even better? +--- + +# Fill the feature suggestion form below and remove this heading + +## What is currently missing? + + + +## How could this be improved? + + + +## Is this a feature you would work on yourself? + +- [ ] I plan to open a pull request for this feature diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..21974fa --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: true +contact_links: + - name: Aiven Security Bug Bounty + url: https://hackerone.com/aiven_ltd + about: Our bug bounty program. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..9e836cd --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,15 @@ + + +# Fill the pull request form below and remove this heading + +## About this change - What it does + + + + + +Resolves: #xxxxx + +## Why this way + + diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..08d9659 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,44 @@ +name: CodeQL + +on: + pull_request: + branches: + - main + types: + - opened + - synchronize + - reopened + - labeled + - unlabeled + push: + branches: + - main + +permissions: + actions: read + contents: read + security-events: write + +jobs: + codeql: + runs-on: ubuntu-latest + if: > + (github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'skip workflows')) || + github.event_name == 'push' + strategy: + fail-fast: false + matrix: + language: + - go + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + - uses: github/codeql-action/init@v3 + with: + languages: "${{ matrix.language }}" + - uses: github/codeql-action/autobuild@v3 + - uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..01e16ab --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,39 @@ +name: Lint + +on: + pull_request: + branches: + - main + types: + - opened + - synchronize + - reopened + - labeled + - unlabeled + push: + branches: + - main + +permissions: read-all + +jobs: + commitlint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - uses: wagoid/commitlint-github-action@v5 + trunk: + if: > + (github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'skip workflows')) || + github.event_name == 'push' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - uses: actions/setup-go@v4 + with: + go-version-file: go.mod + - uses: trunk-io/trunk-action@v1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..086b5a8 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,34 @@ +name: Test + +on: + pull_request: + branches: + - main + types: + - opened + - synchronize + - reopened + - labeled + - unlabeled + push: + branches: + - main + workflow_dispatch: {} + +permissions: read-all + +jobs: + test: + if: > + (github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'skip workflows')) || + github.event_name == 'push' + runs-on: ubuntu-latest + steps: + - uses: actions/setup-go@v4 + with: + go-version-file: go.mod + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - uses: arduino/setup-task@v1 + - run: task test diff --git a/.github/workflows/trunk-upgrade.yml b/.github/workflows/trunk-upgrade.yml new file mode 100644 index 0000000..a003a52 --- /dev/null +++ b/.github/workflows/trunk-upgrade.yml @@ -0,0 +1,28 @@ +name: Upgrade Trunk + +on: + schedule: + - cron: 0 8 * * 1-5 + workflow_dispatch: {} + +permissions: read-all + +jobs: + trunk_upgrade: + runs-on: ubuntu-latest + + permissions: + contents: write + pull-requests: write + + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + - uses: trunk-io/trunk-action/upgrade@v1 + with: + prefix: "ci(deps): " + lowercase-title: true diff --git a/.gitignore b/.gitignore index 1799806..2a319b5 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,211 @@ +# Created by https://www.toptal.com/developers/gitignore/api/macos,linux,windows,go,goland+all,visualstudiocode +# Edit at https://www.toptal.com/developers/gitignore?templates=macos,linux,windows,go,goland+all,visualstudiocode + +### Go ### +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work + +### GoLand+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### GoLand+all Patch ### +# Ignore everything but code style settings and run configurations +# that are supposed to be shared within teams. + +.idea/* + +!.idea/codeStyles +!.idea/runConfigurations + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# End of https://www.toptal.com/developers/gitignore/api/macos,linux,windows,go,goland+all,visualstudiocode + +# Project specific openapi.json diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..0c840a3 --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,55 @@ +run: + build-tags: + - generator + +linters: + disable-all: true + enable: + - durationcheck + - errcheck + - errname + - errorlint + - forbidigo + - forcetypeassert + - funlen + - gocognit + - goconst + - gocyclo + - gofmt + - goimports + - gomnd + - gosec + - gosimple + - govet + - importas + - ineffassign + - lll + - makezero + - misspell + - nakedret + - nestif + - nilerr + - nlreturn + - prealloc + - revive + - staticcheck + - typecheck + - unconvert + - unused + - whitespace + - wsl + +linters-settings: + gocognit: + min-complexity: 25 + goconst: + min-len: 2 + min-occurrences: 2 + govet: + check-shadowing: true + lll: + line-length: 120 + tab-width: 4 + +issues: + exclude-use-default: false diff --git a/.trunk/.gitignore b/.trunk/.gitignore new file mode 100644 index 0000000..15966d0 --- /dev/null +++ b/.trunk/.gitignore @@ -0,0 +1,9 @@ +*out +*logs +*actions +*notifications +*tools +plugins +user_trunk.yaml +user.yaml +tmp diff --git a/.trunk/configs/.markdownlint.yaml b/.trunk/configs/.markdownlint.yaml new file mode 100644 index 0000000..fb94039 --- /dev/null +++ b/.trunk/configs/.markdownlint.yaml @@ -0,0 +1,10 @@ +# Autoformatter friendly markdownlint config (all formatting rules disabled) +default: true +blank_lines: false +bullet: false +html: false +indentation: false +line_length: false +spaces: false +url: false +whitespace: false diff --git a/.trunk/configs/.yamllint.yaml b/.trunk/configs/.yamllint.yaml new file mode 100644 index 0000000..4d44466 --- /dev/null +++ b/.trunk/configs/.yamllint.yaml @@ -0,0 +1,10 @@ +rules: + quoted-strings: + required: only-when-needed + extra-allowed: ["{|}"] + empty-values: + forbid-in-block-mappings: true + forbid-in-flow-mappings: true + key-duplicates: {} + octal-values: + forbid-implicit-octal: true diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml new file mode 100644 index 0000000..da95c59 --- /dev/null +++ b/.trunk/trunk.yaml @@ -0,0 +1,37 @@ +# This file controls the behavior of Trunk: https://docs.trunk.io/cli +# To learn more about the format of this file, see https://docs.trunk.io/reference/trunk-yaml +version: 0.1 +cli: + version: 1.19.0 +# Trunk provides extensibility via plugins. (https://docs.trunk.io/plugins) +plugins: + sources: + - id: trunk + ref: v1.4.2 + uri: https://github.com/trunk-io/plugins +# Many linters and tools depend on runtimes - configure them here. (https://docs.trunk.io/runtimes) +runtimes: + enabled: + - go@1.21.0 + - node@18.12.1 + - python@3.10.8 +# This is the section where you manage your linters. (https://docs.trunk.io/check/configuration) +lint: + enabled: + - actionlint@1.6.26 + - checkov@3.2.5 + - git-diff-check + - gofmt@1.20.4 + - golangci-lint@1.55.2 + - markdownlint@0.39.0 + - osv-scanner@1.6.2 + - prettier@3.2.5 + - trivy@0.49.0 + - trufflehog@3.67.1 + - yamllint@1.33.0 +actions: + enabled: + - trunk-announce + - trunk-check-pre-push + - trunk-fmt-pre-commit + - trunk-upgrade-available diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..a1f7299 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +- Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or + advances of any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email + address, without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +opensource@aiven.io. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..b95da7c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,94 @@ +# Contributing + +## Welcome + +Contributions are very welcome on go-api-schemas. When contributing please keep this in mind: + +- Open an issue to discuss new bigger features. +- Write code consistent with the project style and make sure the tests are passing. +- Stay in touch with us if we have follow up questions or requests for further changes. + +## Architectural Choices + +### Unified Interface Concept + +The `aiven.Client` offers a singular interface to access all Aiven functionalities through `OperationID`, rather than +organizing them into categorized handlers. This design has several advantages: + +1. The permanence of `OperationID` ensures stability in the interface, even when specifications undergo modifications. +2. It simplifies the process of mocking/testing by allowing the creation of client method subsets: + +```go +type sweeperClient interface { + ServiceDelete(ctx context.Context, project string, serviceName string) error + ServiceUserDelete(ctx context.Context, project string, serviceName string, serviceUsername string) error + VpcDelete(ctx context.Context, project string, projectVpcId string) (*vpc.VpcDeleteOut, error) +} + +func sweeper(client serviceShredderClient) error { + ... // cleaning process +} +``` + +### Use of Pointers for Nullable Types + +To differentiate between absent fields and fields with zero-values, the Aiven API uses a distinct approach. In certain +scenarios, it's crucial to avoid sending empty structures or arrays, as they may inadvertently trigger additional +actions, like creating an event log entry when `tech_emails` is sent. The client, therefore, treats `nil` as indicative +of a field's absence. + +### Array Elements Without Pointers + +The code generator is designed not to use pointers for elements within arrays, to avoid the inclusion of `nil` values +within these arrays. Consequently, it necessitates explicit `nil` checks. + +### Distinct Request and Response Structures + +Despite similarities in appearance, request and response structures are intentionally kept distinct, with no shared +codebase except for enums. This is to ensure that changes in either request or response objects don't lead to the +creation of new structures due to naming conflicts, thus enhancing the robustness of the generated code by maintaining +separation: + +```go +type UserIn struct { + Name string `json:"name"` +} + +type UserOut struct { + Name string `json:"name"` +} +``` + +## Development + +### Local Environment + +Place the OpenAPI specification file in the root directory of the project as `openapi.json`, and run the following +command to generate the client code: + +```bash +task generate +``` + +### Tests + +```bash +task test +``` + +### Static checking and Linting + +We use [Trunk.io](https://trunk.io/) for static checking and linting. Install it locally and you'll be good to go. + +## Opening a PR + +- Commit messages should describe the changes, not the filenames. Win our admiration by following the + [excellent advice from Chris Beams](https://chris.beams.io/posts/git-commit/) when composing commit messages. +- Choose a meaningful title for your pull request. +- The pull request description should focus on what changed and why. +- Check that the tests pass (and add test coverage for your changes if appropriate). + +### Commit Messages + +This project adheres to the [Conventional Commits](https://conventionalcommits.org/en/v1.0.0/) specification. +Please, make sure that your commit messages follow that specification. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..48c7708 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) 2021 Aiven, Helsinki, Finland. https://aiven.io/ + + 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/Makefile b/Makefile deleted file mode 100644 index 6dd5db9..0000000 --- a/Makefile +++ /dev/null @@ -1,16 +0,0 @@ -# On MACOS requires gnu-sed. Run `brew info gnu-sed` and follow instructions to replace default sed. -# Negative lookbehind tries to find "= `" pattern to not affect go templates for code generation -imports: - find . -type f -name '*.go' -exec sed -zi 's/(?<== `\s+)"\n\+\t"/"\n"/g' {} + - goimports -local "github.com/aiven/aiven-go-client-v3" -w . - -GEN_HANDLER_DIR ?= handler - -go-generate: - rm -rf $(GEN_HANDLER_DIR) - GEN_HANDLER_DIR=$(GEN_HANDLER_DIR) go run -tags=generator ./generator/... - -generate: go-generate imports - -test: - go test -v ./... diff --git a/README.md b/README.md index 1cffbdb..88ac515 100644 --- a/README.md +++ b/README.md @@ -1,95 +1,48 @@ -# Code-generated Aiven Go Client +# go-client-codegen -## Known limitations +go-client-codegen is an automatically code generated Aiven Go Client. It is generated from the Aiven API specification. -- doesn't support query params -- doesn't support custom certificates -- `string` type is never a pointer, as it is not expected to send `""` -- authorization is by token only -- slices never omitted (always sent), as it makes development much easier +## Setup -## Installation - -``` shell -go get github.com/aiven/aiven-go-client-v3 +```bash +go get github.com/aiven/go-client-codegen ``` -## Configuration and usage - -### via env vars +### Configuration and Usage -| Name | Type | Description | -|--------------------|:---------|------------------------| -| `AIVEN_TOKEN` | `string` | API auth token | -| `AIVEN_WEB_URL` | `string` | API Server location | -| `AIVEN_USER_AGENT` | `string` | Client user agent | -| `AIVEN_DEBUG` | `bool` | Enables Stderr logging | +#### Via Environment Variables +| Name | Type | Description | +| ------------------ | :------- | ------------------------------ | +| `AIVEN_TOKEN` | `string` | Aiven API Authentication Token | +| `AIVEN_WEB_URL` | `string` | Aiven API URL | +| `AIVEN_USER_AGENT` | `string` | User Agent | +| `AIVEN_DEBUG` | `bool` | Debug Output Flag (stderr) | -### via constructor options +#### Via Constructor Options ```go -import "github.com/aiven/aiven-go-client-v3" +import "github.com/aiven/go-client-codegen" -client, err := aiven.NewClient(DebugOpt(true), UserAgentOpt("smith")) +client, err := aiven.NewClient(DebugOpt(true), UserAgentOpt("foo")) if err != nil { return err } -services, err := client.ServiceList(ctx, "my-project") +services, err := client.ServiceList(ctx, "bar-project") ``` -## Design decisions - -### Al-in-one interface - -The `aiven.Client` exposes all Aiven methods by OperationID instead of providing with scoped/grouped handlers. -This approach has several benefits: - -1. The OperationID is immutable, means the interface should not dramatically change if spec is changed -2. Easier mocking/testing, as it is possible to create a subset of client methods: +See [CONTRIBUTING.md](CONTRIBUTING.md) for instructions on how to contribute to the development of go-client-codegen. -```go -type sweeperClient interface { - ServiceDelete(ctx context.Context, project string, serviceName string) error - ServiceUserDelete(ctx context.Context, project string, serviceName string, serviceUsername string) error - VpcDelete(ctx context.Context, project string, projectVpcId string) (*vpc.VpcDeleteOut, error) -} - -func sweeper(client serviceShredderClient) error { - ... // sweep -} -``` +## License -### Pointers for reference types +go-client-codegen is licensed under the Apache license, version 2.0. Full license text is available in the +[LICENSE](LICENSE) file. -The Aiven API distinguishes between cases when the field is missing or has a zero-value. -In some cases, we must avoid sending empty arrays or objects even if that works as expected. -For instance, sending `tech_emails` triggers creation of an additional event log entry in Console. -As a universal solution, the client takes `nil` as "missing". +Please note that the project explicitly does not require a CLA (Contributor License Agreement) from its contributors. -### `[]Foo`, not `[]*Foo` - -The generator doesn't create pointers for array elements. -Because technically that means it might contain `nil` values. -Therefore `nil` checks _must_ be performed. - -### Response objects - -Request and response objects are separated and do not share code, except enums. -Even though if they look similar: - -```go -type UserIn struct { - Name string `json:"name"` -} - -type UserOut struct { - Name string `json:"name"` -} -``` +## Contact -That's made on purpose, so if a request or response object has been changed, hence `UserIn != UserOut` -it won't generate a new struct. -Which in turn might regenerate other objects because of the name collision. -Keeping things separate makes generated code more durable. +Bug reports and patches are very welcome, please post them as GitHub issues and pull requests at +https://github.com/aiven/go-client-codegen. To report any possible vulnerabilities or other serious issues please see +our [security](SECURITY.md) policy. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..4aae85e --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,32 @@ +# Security Policy + +## Supported Versions + +We release patches for security vulnerabilities. Which versions are eligible to receive such patches depend on the +CVSS v3.0 Rating: + +| CVSS v3.0 | Supported Versions | +| --------- | ------------------- | +| 4.0-10.0 | Most recent release | + +## Reporting a Vulnerability + +Please report (suspected) security vulnerabilities to our **[bug bounty program](https://hackerone.com/aiven_ltd)**. +You will receive a response from us within 2 working days. If the issue is confirmed, we will release a patch as soon +as possible depending on impact and complexity. + +## Qualifying Vulnerabilities + +Any reproducible vulnerability that has a severe effect on the security or privacy of our users is likely to be in +scope for the program. + +We generally **aren't** interested in the following issues: + +- Social engineering (e.g. phishing, vishing, smishing) attacks +- Brute force, DoS, text injection +- Missing best practices such as HTTP security headers (CSP, X-XSS, etc.), email (SPF/DKIM/DMARC records), SSL/TLS + configuration. +- Software version disclosure / Banner identification issues / Descriptive error messages or headers (e.g. stack + traces, application or server errors). +- Clickjacking on pages with no sensitive actions +- Theoretical vulnerabilities where you can't demonstrate a significant security impact with a proof of concept. diff --git a/Taskfile.yml b/Taskfile.yml new file mode 100644 index 0000000..8bdd7a0 --- /dev/null +++ b/Taskfile.yml @@ -0,0 +1,21 @@ +version: 3 + +vars: + GEN_OUT_DIR: handler + +tasks: + fmt-imports: + cmds: + - find . -type f -name '*.go' -exec sed -zi 's/(?<== `\s+)"\n\+\t"/"\n"/g' {} + + - goimports -local "github.com/aiven/go-client-codegen" -w . + go-generate: + cmds: + - rm -rf {{.GEN_OUT_DIR}} + - GEN_OUT_DIR={{.GEN_OUT_DIR}} go run -tags=generator ./generator/... + generate: + cmds: + - task: go-generate + - task: fmt-imports + test: + cmds: + - go test -v ./... diff --git a/client.go b/client.go index eb04452..e2db1b0 100644 --- a/client.go +++ b/client.go @@ -1,3 +1,4 @@ +// Package aiven provides a client for interacting with the Aiven API. package aiven import ( @@ -17,10 +18,14 @@ import ( "github.com/rs/zerolog" ) -var errTokenIsRequired = errors.New("token is required. See https://api.aiven.io/doc/#section/Get-started/Authentication") +var errTokenIsRequired = errors.New( + "token is required. See https://api.aiven.io/doc/#section/Get-started/Authentication", +) +// NewClient creates a new Aiven client. func NewClient(opts ...Option) (Client, error) { d := new(aivenClient) + err := envconfig.Process("", d) if err != nil { return nil, err @@ -57,23 +62,28 @@ type aivenClient struct { doer Doer } +// OperationIDKey is the key used to store the operation ID in the context. type OperationIDKey struct{} func (d *aivenClient) Do(ctx context.Context, operationID, method, path string, v any) ([]byte, error) { ctx = context.WithValue(ctx, OperationIDKey{}, operationID) var rsp *http.Response + var err error + if d.Debug { start := time.Now() defer func() { end := time.Since(start) + var event *zerolog.Event if err != nil { event = d.logger.Error().Err(err) } else { event = d.logger.Info().Str("status", rsp.Status) } + event.Ctx(ctx). Stringer("took", end). Str("operationID", operationID). @@ -102,11 +112,13 @@ func (d *aivenClient) Do(ctx context.Context, operationID, method, path string, func (d *aivenClient) do(ctx context.Context, method, path string, v any) (*http.Response, error) { var body io.Reader + if !(v == nil || isEmpty(v)) { b, err := json.Marshal(v) if err != nil { return nil, err } + body = bytes.NewBuffer(b) } @@ -125,11 +137,13 @@ func (d *aivenClient) do(ctx context.Context, method, path string, v any) (*http query := req.URL.Query() query.Add("limit", "999") req.URL.RawQuery = query.Encode() + return d.doer.Do(req) } func isEmpty(a any) bool { v := reflect.ValueOf(a) + return !v.IsZero() || v.Kind() == reflect.Ptr && v.IsNil() } diff --git a/client_generated.go b/client_generated.go index 264fccc..f5e281d 100644 --- a/client_generated.go +++ b/client_generated.go @@ -1,43 +1,43 @@ -// Code generated by Aiven. DO NOT EDIT. +// Code generated by go-client-codegen. DO NOT EDIT. package aiven import ( "context" - account "github.com/aiven/aiven-go-client-v3/handler/account" - accountauthentication "github.com/aiven/aiven-go-client-v3/handler/accountauthentication" - accountteam "github.com/aiven/aiven-go-client-v3/handler/accountteam" - accountteammember "github.com/aiven/aiven-go-client-v3/handler/accountteammember" - billinggroup "github.com/aiven/aiven-go-client-v3/handler/billinggroup" - clickhouse "github.com/aiven/aiven-go-client-v3/handler/clickhouse" - cloud "github.com/aiven/aiven-go-client-v3/handler/cloud" - domain "github.com/aiven/aiven-go-client-v3/handler/domain" - flink "github.com/aiven/aiven-go-client-v3/handler/flink" - flinkapplication "github.com/aiven/aiven-go-client-v3/handler/flinkapplication" - flinkapplicationdeployment "github.com/aiven/aiven-go-client-v3/handler/flinkapplicationdeployment" - flinkapplicationversion "github.com/aiven/aiven-go-client-v3/handler/flinkapplicationversion" - flinkjob "github.com/aiven/aiven-go-client-v3/handler/flinkjob" - kafka "github.com/aiven/aiven-go-client-v3/handler/kafka" - kafkaconnect "github.com/aiven/aiven-go-client-v3/handler/kafkaconnect" - kafkamirrormaker "github.com/aiven/aiven-go-client-v3/handler/kafkamirrormaker" - kafkaschemaregistry "github.com/aiven/aiven-go-client-v3/handler/kafkaschemaregistry" - kafkatopic "github.com/aiven/aiven-go-client-v3/handler/kafkatopic" - mysql "github.com/aiven/aiven-go-client-v3/handler/mysql" - opensearch "github.com/aiven/aiven-go-client-v3/handler/opensearch" - organization "github.com/aiven/aiven-go-client-v3/handler/organization" - organizationuser "github.com/aiven/aiven-go-client-v3/handler/organizationuser" - postgresql "github.com/aiven/aiven-go-client-v3/handler/postgresql" - privatelink "github.com/aiven/aiven-go-client-v3/handler/privatelink" - project "github.com/aiven/aiven-go-client-v3/handler/project" - projectbilling "github.com/aiven/aiven-go-client-v3/handler/projectbilling" - service "github.com/aiven/aiven-go-client-v3/handler/service" - serviceintegration "github.com/aiven/aiven-go-client-v3/handler/serviceintegration" - serviceuser "github.com/aiven/aiven-go-client-v3/handler/serviceuser" - staticip "github.com/aiven/aiven-go-client-v3/handler/staticip" - user "github.com/aiven/aiven-go-client-v3/handler/user" - usergroup "github.com/aiven/aiven-go-client-v3/handler/usergroup" - vpc "github.com/aiven/aiven-go-client-v3/handler/vpc" + account "github.com/aiven/go-client-codegen/handler/account" + accountauthentication "github.com/aiven/go-client-codegen/handler/accountauthentication" + accountteam "github.com/aiven/go-client-codegen/handler/accountteam" + accountteammember "github.com/aiven/go-client-codegen/handler/accountteammember" + billinggroup "github.com/aiven/go-client-codegen/handler/billinggroup" + clickhouse "github.com/aiven/go-client-codegen/handler/clickhouse" + cloud "github.com/aiven/go-client-codegen/handler/cloud" + domain "github.com/aiven/go-client-codegen/handler/domain" + flink "github.com/aiven/go-client-codegen/handler/flink" + flinkapplication "github.com/aiven/go-client-codegen/handler/flinkapplication" + flinkapplicationdeployment "github.com/aiven/go-client-codegen/handler/flinkapplicationdeployment" + flinkapplicationversion "github.com/aiven/go-client-codegen/handler/flinkapplicationversion" + flinkjob "github.com/aiven/go-client-codegen/handler/flinkjob" + kafka "github.com/aiven/go-client-codegen/handler/kafka" + kafkaconnect "github.com/aiven/go-client-codegen/handler/kafkaconnect" + kafkamirrormaker "github.com/aiven/go-client-codegen/handler/kafkamirrormaker" + kafkaschemaregistry "github.com/aiven/go-client-codegen/handler/kafkaschemaregistry" + kafkatopic "github.com/aiven/go-client-codegen/handler/kafkatopic" + mysql "github.com/aiven/go-client-codegen/handler/mysql" + opensearch "github.com/aiven/go-client-codegen/handler/opensearch" + organization "github.com/aiven/go-client-codegen/handler/organization" + organizationuser "github.com/aiven/go-client-codegen/handler/organizationuser" + postgresql "github.com/aiven/go-client-codegen/handler/postgresql" + privatelink "github.com/aiven/go-client-codegen/handler/privatelink" + project "github.com/aiven/go-client-codegen/handler/project" + projectbilling "github.com/aiven/go-client-codegen/handler/projectbilling" + service "github.com/aiven/go-client-codegen/handler/service" + serviceintegration "github.com/aiven/go-client-codegen/handler/serviceintegration" + serviceuser "github.com/aiven/go-client-codegen/handler/serviceuser" + staticip "github.com/aiven/go-client-codegen/handler/staticip" + user "github.com/aiven/go-client-codegen/handler/user" + usergroup "github.com/aiven/go-client-codegen/handler/usergroup" + vpc "github.com/aiven/go-client-codegen/handler/vpc" ) type doer interface { diff --git a/client_test.go b/client_test.go index 654841c..2dfd156 100644 --- a/client_test.go +++ b/client_test.go @@ -1,3 +1,4 @@ +// Package aiven provides a client for interacting with the Aiven API. package aiven import ( @@ -24,10 +25,12 @@ func TestNewClient(t *testing.T) { require.NoError(t, err) found := 0 + for _, to := range tokens { if strings.HasPrefix(token, to.TokenPrefix) { found++ } } + assert.Equal(t, 1, found) } diff --git a/commitlint.config.js b/commitlint.config.js new file mode 100644 index 0000000..2ef25a1 --- /dev/null +++ b/commitlint.config.js @@ -0,0 +1,4 @@ +module.exports = { + extends: ["@commitlint/config-conventional"], + ignores: [(msg) => /Signed-off-by: dependabot\[bot]/m.test(msg)], +}; diff --git a/error.go b/error.go index b40e672..f29076a 100644 --- a/error.go +++ b/error.go @@ -1,3 +1,4 @@ +// Package aiven provides a client for interacting with the Aiven API. package aiven import ( @@ -22,12 +23,14 @@ func (e Error) Error() string { // IsNotFound returns true if the specified error has status 404 func IsNotFound(err error) bool { var e Error + return errors.As(err, &e) && e.Status == http.StatusNotFound } // IsAlreadyExists returns true if the error message and error code that indicates that entity already exists func IsAlreadyExists(err error) bool { var e Error + return errors.As(err, &e) && strings.Contains(e.Message, "already exists") && e.Status == http.StatusConflict } @@ -37,5 +40,6 @@ func OmitNotFound(err error) error { if IsNotFound(err) { return nil } + return err } diff --git a/error_test.go b/error_test.go index 11e3846..e2683c4 100644 --- a/error_test.go +++ b/error_test.go @@ -1,3 +1,4 @@ +// Package aiven provides a client for interacting with the Aiven API. package aiven import ( diff --git a/generator/main.go b/generator/main.go index 11b17aa..c085b2e 100644 --- a/generator/main.go +++ b/generator/main.go @@ -1,5 +1,6 @@ //go:build generator +// Package main is the generator of the client code. package main import ( @@ -20,13 +21,13 @@ import ( ) const ( - generatedHeader = "Code generated by Aiven. DO NOT EDIT." + generatedHeader = "Code generated by go-client-codegen. DO NOT EDIT." versionIDParam = `/{version_id:latest|\d+}` configPrefix = "GEN" ) type config struct { - Module string `envconfig:"MODULE" default:"github.com/aiven/aiven-go-client-v3"` + Module string `envconfig:"MODULE" default:"github.com/aiven/go-client-codegen"` Package string `envconfig:"PACKAGE" default:"aiven"` HandlerDir string `envconfig:"HANDLER_DIR" default:"handler"` ConfigFile string `envconfig:"CONFIG_FILE" default:"config.yaml"` @@ -43,8 +44,10 @@ func main() { } } +// nolint:funlen,gocognit,gocyclo // It's a generator, it's supposed to be long, and we won't expand it. func exec() error { cfg := new(config) + err := envconfig.Process(configPrefix, cfg) if err != nil { return err @@ -56,6 +59,7 @@ func exec() error { } config := make(map[string][]string) + err = yaml.Unmarshal(configBytes, &config) if err != nil { return err @@ -67,12 +71,14 @@ func exec() error { } doc := new(Doc) + err = json.Unmarshal(docBytes, doc) if err != nil { return err } pkgs := make(map[string][]*Path) + for path, v := range doc.Paths { for meth, p := range v { if p.Deprecated { @@ -89,6 +95,7 @@ func exec() error { for _, id := range idList { if p.ID == id { pkg = k + break outer } } @@ -96,14 +103,17 @@ func exec() error { if pkg == "" { log.Printf("%q id not found in config. Skipping", p.ID) + continue } pkgs[pkg] = append(pkgs[pkg], p) params := make([]*Parameter, 0, len(p.Parameters)) + for _, ref := range p.Parameters { parts := strings.Split(ref.Ref, "/") name := parts[len(parts)-1] + param, ok := doc.Components.Parameters[name] if !ok { return fmt.Errorf("param %q not found", ref.Ref) @@ -143,12 +153,14 @@ func exec() error { clientTypeValues := make([]jen.Code, 0, len(pkgs)) for _, pkg := range sortedKeys(pkgs) { + const handlerType = "Handler" + paths := pkgs[pkg] fileName := strings.ToLower(pkg) - handlerName := pkg + "Handler" - handlerType := "Handler" + handlerName := pkg + handlerType newHandler := "New" + handlerType scope := make(map[string]*Schema) + for _, p := range paths { p.FuncName = p.OperationID } @@ -165,18 +177,22 @@ func exec() error { ) file.Add(doer) file.Type().Id(handlerName).Struct(jen.Id(doerName).Id(doerName)) - typeMethods := make([]jen.Code, len(paths)) + + var typeMethods []jen.Code + for _, path := range paths { // todo: support 204 out := path.Out.OK.Content["application/json"] if out == nil && path.Out.NoContent.Content == nil { log.Printf("%q has no json response. Skipping", path.OperationID) + continue } schemas := make([]*Schema, 0) params := make([]jen.Code, 0, len(path.Parameters)) params = append(params, ctx) + for _, p := range path.Parameters { p.Schema.in = true p.Schema.required = true @@ -188,11 +204,15 @@ func exec() error { in := path.In.Content["application/json"] if in != nil { - schemaIn, err := doc.getSchema(in.Schema.Ref) + var schemaIn *Schema + + schemaIn, err = doc.getSchema(in.Schema.Ref) if err != nil { return err } + schemaIn.in = true + schemaIn.init(doc, scope, path.FuncName) schemas = append(schemas, schemaIn) params = append(params, jen.Id("in").Id("*"+schemaIn.CamelName)) @@ -207,7 +227,9 @@ func exec() error { if err != nil { return err } + schemaOut.out = true + schemaOut.init(doc, scope, path.FuncName) rsp = getResponse(schemaOut) } @@ -221,6 +243,7 @@ func exec() error { // foo() (*Foo, err) ret = jen.List(jen.Id("*"+rsp.CamelName), jen.Error()) } + typeMeth.Parens(ret) structMeth.Parens(ret) } else { @@ -245,22 +268,27 @@ func exec() error { urlParams := make([]jen.Code, 0, len(params)) urlParams = append(urlParams, jen.Lit(url)) inObj := jen.Nil() + for _, s := range schemas { if s.isObject() { inObj = jen.Id("in") + continue } + urlParams = append(urlParams, jen.Id(strcase.ToLowerCamel(s.CamelName))) } outObj := jen.Id("_") returnErr := jen.Return(jen.Err()) + if rsp != nil { outObj = jen.Id("b") // In most cases, "nil" is for error return // But for required scalars should be zero values returnErr = jen.Return(jen.Nil(), jen.Err()) + if rsp.required || rsp.Type == SchemaTypeString { switch rsp.Type { case SchemaTypeString: @@ -315,6 +343,7 @@ func exec() error { sort.Slice(scopeValues, func(i, j int) bool { return scopeValues[i].CamelName < scopeValues[j].CamelName }) + for _, v := range scopeValues { err = writeStruct(file, v) if err != nil { @@ -323,12 +352,14 @@ func exec() error { } dirPath := filepath.Join(cfg.HandlerDir, fileName) + err = os.MkdirAll(dirPath, os.ModePerm) if err != nil { return err } handler.Interface(typeMethods...) + err = file.Save(filepath.Join(dirPath, fileName+".go")) if err != nil { return err @@ -348,23 +379,28 @@ func exec() error { ) client.Type().Id("client").Struct(clientFields...) client.Type().Id("Client").Interface(clientTypeValues...) + return client.Save(cfg.ClientFile) } // reMakesSense sometimes there are invalid enums, for instance just a comma "," var reMakesSense = regexp.MustCompile(`\w`) +// nolint:funlen // It's a generator, it's supposed to be long, and we won't expand it. func writeStruct(f *jen.File, s *Schema) error { if s.isMap() || s.isArray() || s.isScalar() && !s.isEnum() { return nil } + // nolint:nestif // It's a generator, it's supposed to be long, and we won't expand it. if s.isEnum() { kind := getScalarType(s) o := f.Type().Id(s.CamelName) o.Add(kind) + enums := make([]jen.Code, 0) values := make([]jen.Code, 0) + for _, e := range s.Enum { literal := fmt.Sprint(e) if !reMakesSense.MatchString(literal) { @@ -382,6 +418,7 @@ func writeStruct(f *jen.File, s *Schema) error { if strings.HasSuffix(literal, "*") { constant += "Asterisk" } + enums = append(enums, jen.Id(constant).Op(s.CamelName).Op("=").Lit(literal)) values = append(values, jen.Lit(literal)) } @@ -397,22 +434,27 @@ func writeStruct(f *jen.File, s *Schema) error { jen.Return(jen.Index().Add(kind).Values(values...)), ) } + return nil } fields := make([]jen.Code, 0, len(s.Properties)) + for _, k := range s.propertyNames { p := s.Properties[k] field := jen.Id(strcase.ToCamel(k)).Add(getType(p)) tag := k + if !p.required { tag += ",omitempty" } + field = field.Tag(map[string]string{"json": strings.ReplaceAll(tag, `\`, "")}) fields = append(fields, field) } f.Type().Id(s.CamelName).Struct(fields...) + return nil } @@ -425,6 +467,7 @@ func getResponse(s *Schema) *Schema { case 0: return nil } + return s } @@ -433,5 +476,6 @@ func toSingle(src string) string { if s != src { return s + "y" } + return strings.TrimSuffix(src, "s") } diff --git a/generator/models.go b/generator/models.go index a91b600..18c68f3 100644 --- a/generator/models.go +++ b/generator/models.go @@ -15,6 +15,7 @@ import ( const docSite = "https://api.aiven.io/doc" +// Doc represents a parsed OpenAPI document. type Doc struct { Paths map[string]map[string]*Path `json:"paths"` Components struct { @@ -25,20 +26,25 @@ type Doc struct { func (d *Doc) getSchema(path string) (*Schema, error) { name := strings.Split(path, "/")[3] + schema := d.Components.Schemas[name] if schema == nil { return nil, fmt.Errorf("schema %q not found", path) } + schema.name = name + return schema, nil } +// Content represents a request or response body. type Content map[string]*struct { Schema struct { Ref string `json:"$ref"` } `json:"schema"` } +// Path represents a parsed OpenAPI path. type Path struct { ID string Path string @@ -62,6 +68,7 @@ type Path struct { } `json:"responses"` } +// Comment returns a comment for the path. func (p *Path) Comment() *jen.Statement { // IDE highlights any coincidence method names // For instance, there is always "List" method and that's a common verb @@ -70,20 +77,25 @@ func (p *Path) Comment() *jen.Statement { s := lowerFirst(p.Summary) c := jen.Comment(fmt.Sprintf("%s %s", p.FuncName, s)) c.Line().Comment(fmt.Sprintf("%s %s", p.Method, p.Path)) + if p.Tags[0] == "" { c.Line().Comment(fmt.Sprintf("%s/#operation/%s", docSite, p.OperationID)) } else { c.Line().Comment(fmt.Sprintf("%s/#tag/%s/operation/%s", docSite, p.Tags[0], p.OperationID)) } + return c } +// ParameterIn represents a parameter location. type ParameterIn string const ( + // ParameterInPath represents a path parameter location. ParameterInPath ParameterIn = "path" ) +// Parameter represents a parsed OpenAPI parameter. type Parameter struct { Ref string `json:"$ref"` In ParameterIn `json:"in"` @@ -93,18 +105,27 @@ type Parameter struct { Schema *Schema `json:"schema"` } +// SchemaType represents a schema type. type SchemaType string const ( - SchemaTypeObject = "object" - SchemaTypeArray = "array" - SchemaTypeString = "string" + // SchemaTypeObject represents an object schema type. + SchemaTypeObject = "object" + // SchemaTypeArray represents an array schema type. + SchemaTypeArray = "array" + // SchemaTypeString represents a string schema type. + SchemaTypeString = "string" + // SchemaTypeInteger represents an integer schema type. SchemaTypeInteger = "integer" - SchemaTypeNumber = "number" + // SchemaTypeNumber represents a number schema type. + SchemaTypeNumber = "number" + // SchemaTypeBoolean represents a boolean schema type. SchemaTypeBoolean = "boolean" - SchemaTypeTime = "time" + // SchemaTypeTime represents a time schema type. + SchemaTypeTime = "time" ) +// Schema represents a parsed OpenAPI schema. type Schema struct { Type SchemaType `json:"type"` Properties map[string]*Schema `json:"properties"` @@ -122,12 +143,14 @@ type Schema struct { in, out bool // Request or Response DTO } +// nolint:funlen,gocognit // It is easy to maintain and read, we don't need to split it func (s *Schema) init(doc *Doc, scope map[string]*Schema, name string) { if s.Ref != "" { other, err := doc.getSchema(s.Ref) if err != nil { panic(err) } + *s = *other } @@ -148,13 +171,16 @@ func (s *Schema) init(doc *Doc, scope map[string]*Schema, name string) { s.name = name s.CamelName = strcase.ToCamel(s.name) + if s.isEnum() { - if !strings.HasSuffix(s.CamelName, "Type") { - s.CamelName += "Type" + const enumTypeSuffix = "Type" + + if !strings.HasSuffix(s.CamelName, enumTypeSuffix) { + s.CamelName += enumTypeSuffix } // When it is just "Type" it is useless - if s.CamelName == "Type" { + if s.CamelName == enumTypeSuffix { s.CamelName = s.parent.CamelName + s.CamelName } } @@ -212,11 +238,14 @@ func (s *Schema) init(doc *Doc, scope map[string]*Schema, name string) { // A duplicate if other.CamelName == s.CamelName { s.CamelName += "Alt" + continue outer } } + break outer } + scope[s.hash()] = s } @@ -240,6 +269,7 @@ func (s *Schema) hash() string { if s.isEnum() { return mustMarshal(s.Enum) } + return mustMarshal(s) } @@ -256,6 +286,7 @@ func (s *Schema) isScalar() bool { case SchemaTypeString, SchemaTypeInteger, SchemaTypeNumber, SchemaTypeBoolean, SchemaTypeTime: return true } + return false } @@ -272,6 +303,7 @@ func (s *Schema) root() *Schema { if s.parent == nil { return s } + return s.parent.root() } @@ -311,6 +343,7 @@ func getType(s *Schema) *jen.Statement { if !s.required && s.Type != SchemaTypeString { return jen.Op("*").Add(scalar) } + return scalar } @@ -325,17 +358,20 @@ func getType(s *Schema) *jen.Statement { if s.Items.isObject() || s.Items.isArray() { return a.Id(s.Items.CamelName) } + return a.Add(getType(s.Items)) case s.isObject(): if !s.required { return jen.Id("*" + s.CamelName) } + return jen.Id(s.CamelName) case s.isMap(): a := jen.Map(jen.String()) if !(s.required || s.isOut()) { a = jen.Op("*").Map(jen.String()) } + if isMapString(s) { return a.String() } else { @@ -351,6 +387,7 @@ func mustMarshal(s any) string { if err != nil { panic(err) } + return string(b) } @@ -363,12 +400,9 @@ func lowerFirst(s string) string { return strings.ToLower(s[:1]) + s[1:] } -func upperFirst(s string) string { - return strings.ToUpper(s[:1]) + s[1:] -} - func sortedKeys[T any](m map[string]T) []string { keys := maps.Keys(m) sort.Strings(keys) + return keys } diff --git a/go.mod b/go.mod index ecfc01c..4f677ec 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,4 @@ -module github.com/aiven/aiven-go-client-v3 - -replace github.com/aiven/aiven-go-client-v3 => ../. +module github.com/aiven/go-client-codegen go 1.21 @@ -10,7 +8,7 @@ require ( github.com/hashicorp/go-retryablehttp v0.7.5 github.com/iancoleman/strcase v0.3.0 github.com/kelseyhightower/envconfig v1.4.0 - github.com/rs/zerolog v1.31.0 + github.com/rs/zerolog v1.32.0 github.com/stretchr/testify v1.8.4 golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 gopkg.in/yaml.v3 v3.0.1 @@ -23,6 +21,5 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/samber/lo v1.39.0 // indirect golang.org/x/sys v0.16.0 // indirect ) diff --git a/go.sum b/go.sum index c093212..143082a 100644 --- a/go.sum +++ b/go.sum @@ -29,10 +29,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= -github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= -github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA= -github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= +github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0= +github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= diff --git a/option.go b/option.go index c097f55..3eb3801 100644 --- a/option.go +++ b/option.go @@ -1,5 +1,7 @@ +// Package aiven provides a client for interacting with the Aiven API. package aiven +// Option is a function that configures the client. type Option func(*aivenClient) // DebugOpt whether should the client run in debug mode. diff --git a/retry.go b/retry.go index b911b75..71d0c1f 100644 --- a/retry.go +++ b/retry.go @@ -1,3 +1,4 @@ +// Package aiven provides a client for interacting with the Aiven API. package aiven import ( @@ -17,6 +18,7 @@ import ( // Suspends errors, cause that's what retryablehttp.DefaultRetryPolicy does func checkRetry(ctx context.Context, rsp *http.Response, err error) (bool, error) { shouldRetry, err := retryablehttp.ErrorPropagatedRetryPolicy(ctx, rsp, err) + return shouldRetry || err == nil && isRetryable(rsp), nil } @@ -39,7 +41,13 @@ func isRetryable(rsp *http.Response) bool { case http.StatusNotFound: // We need to restore the body body := rsp.Body - defer body.Close() + + defer func(body io.ReadCloser) { + err := body.Close() + if err != nil { + panic(err) + } + }(body) // Shouldn't be there much of data, ReadAll is ok b, err := io.ReadAll(body) @@ -58,6 +66,7 @@ func isRetryable(rsp *http.Response) bool { } } } + return false }