diff --git a/.github/workflows/license-header-check.yml b/.github/workflows/license-header-check.yml new file mode 100644 index 0000000..52ebae5 --- /dev/null +++ b/.github/workflows/license-header-check.yml @@ -0,0 +1,18 @@ +--- +# Copyright The Linux Foundation and each contributor to LFX. +# SPDX-License-Identifier: MIT +name: License Header Check + +"on": [pull_request] + +permissions: + contents: read + pull-requests: write + +jobs: + license-header-check: + name: License Header Check + uses: linuxfoundation/lfx-public-workflows/.github/workflows/license-header-check.yml@main + with: + copyright_line: "Copyright The Linux Foundation and each contributor to LFX." + exclude_pattern: "gen/*" diff --git a/.github/workflows/mega-linter.yml b/.github/workflows/mega-linter.yml new file mode 100644 index 0000000..e33161a --- /dev/null +++ b/.github/workflows/mega-linter.yml @@ -0,0 +1,42 @@ +--- +# Copyright The Linux Foundation and each contributor to LFX. +# SPDX-License-Identifier: MIT + +name: MegaLinter + +"on": + pull_request: null + +permissions: + contents: read + +concurrency: + group: ${{ github.ref }}-${{ github.workflow }} + cancel-in-progress: true + +jobs: + megalinter: + name: MegaLinter + runs-on: ubuntu-latest + + permissions: + contents: read + + steps: + # Git Checkout + - name: Checkout Code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + fetch-depth: 0 + + # MegaLinter + - name: MegaLinter + id: ml + # Use the Go flavor. + uses: oxsecurity/megalinter/flavors/go@5a91fb06c83d0e69fbd23756d47438aa723b4a5a # 8.7.0 + env: + # All available variables are described in documentation + # https://megalinter.io/configuration/ + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Allow GITHUB_TOKEN for working around rate limits (aquasecurity/trivy#7668). + REPOSITORY_TRIVY_UNSECURED_ENV_VARIABLES: GITHUB_TOKEN diff --git a/.github/workflows/project-api-build.yml b/.github/workflows/project-api-build.yml new file mode 100644 index 0000000..3d88524 --- /dev/null +++ b/.github/workflows/project-api-build.yml @@ -0,0 +1,38 @@ +# Copyright The Linux Foundation and each contributor to LFX. +# SPDX-License-Identifier: MIT +--- +name: "Project API Build" + +"on": [pull_request] + +permissions: + contents: read + +jobs: + build-pr: + name: Build and Test + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Setup go + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + with: + go-version: stable + + - name: Download Dependencies + run: make deps + working-directory: cmd/project-api + + - name: Generate service code + run: make apigen + working-directory: cmd/project-api + + - name: Build + run: make build + working-directory: cmd/project-api + + - name: Test + run: make test + working-directory: cmd/project-api diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..91fe8d3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +# Copyright The Linux Foundation and each contributor to LFX. +# SPDX-License-Identifier: MIT +bin/ +.env* + +# Linter generated files +megalinter-reports/ +revive.log \ No newline at end of file diff --git a/.gitleaks.toml b/.gitleaks.toml new file mode 100644 index 0000000..c10d002 --- /dev/null +++ b/.gitleaks.toml @@ -0,0 +1,30 @@ +# Copyright The Linux Foundation and each contributor to LFX. +# SPDX-License-Identifier: MIT + +title = "gitleaks config" + +[extend] +# useDefault will extend the base configuration with the default gitleaks config: +# https://github.com/zricethezav/gitleaks/blob/master/config/gitleaks.toml +useDefault = true + +[allowlist] +description = "Allowlisted files" +paths = [ + '''.automation/test''', + '''megalinter-reports''', + '''.github/linters''', + '''node_modules''', + '''.mypy_cache''', + '''./cmd/project-api/service_handler_test.go''', + '''./cmd/project-api/service_endpoint_test.go''', + '''(.*?)gitleaks\.toml$''', + '''(?i)(.*?)(png|jpeg|jpg|gif|doc|docx|pdf|bin|xls|xlsx|pyc|zip)$''', + '''(go.mod|go.sum)$''', + '''(.*?)(swagger\.yml|swagger\.yaml)$''', + '''(.*?)(serverless\.yml|serverless\.yaml)$''', +] +regexTarget = "match" +regexes = [ + '''eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.iOeNU4dAFFeBwNj6qdhdvm-IvDQrTa6R22lQVJVuWJxorJfeQww5Nwsra0PjaOYhAMj9jNMO5YLmud8U7iQ5gJK2zYyepeSuXhfSi8yjFZfRiSkelqSkU19I-Ja8aQBDbqXf2SAWA8mHF8VS3F08rgEaLCyv98fLLH4vSvsJGf6ueZSLKDVXz24rZRXGWtYYk_OYYTVgR1cg0BLCsuCvqZvHleImJKiWmtS0-CymMO4MMjCy_FIl6I56NqLE9C87tUVpo1mT-kbg5cHDD8I7MjCW5Iii5dethB4Vid3mZ6emKjVYgXrtkOQ-JyGMh6fnQxEFN1ft33GX2eRHluK9eg''', +] diff --git a/.gitleaksignore b/.gitleaksignore new file mode 100644 index 0000000..b1ec9a5 --- /dev/null +++ b/.gitleaksignore @@ -0,0 +1,2 @@ +46d11606320853a027a623c203238656cda3d36e:service_test.go:jwt:571 +874abaf0c197e135ec27253c169f6b2deead5806:service_test.go:jwt:571 diff --git a/.mega-linter.yml b/.mega-linter.yml new file mode 100644 index 0000000..b319383 --- /dev/null +++ b/.mega-linter.yml @@ -0,0 +1,42 @@ +# Copyright The Linux Foundation and each contributor to LFX. +# SPDX-License-Identifier: MIT +--- +GITHUB_COMMENT_REPORTER: false +DISABLE_LINTERS: + # We are setting KUBERNETES_DIRECTORY to the helm chart so the + # KUBERNETES_HELM linter can find the chart, but then this linter expects to + # find raw Kubernetes manifests in this directory, which isn't the case. + # If we added a PRE_ command with a `helm template` step, and further + # restricted the included files for this linter, it *might* work. + - KUBERNETES_KUBECONFORM + # TBD how to use this from Megalinter with our setup. + - KUBERNETES_KUBESCAPE + # Repository-wide link checking returns mostly false positives (like internal + # service URLs in templates). + - SPELL_LYCHEE + - SPELL_CSPELL + # yamllint is sufficient for us. + - YAML_PRETTIER +DISABLE_ERRORS_LINTERS: + # This may be informative but doesn't need to break the build. + - COPYPASTE_JSCPD + # TBD! Need to work through these. + - REPOSITORY_TRIVY + - REPOSITORY_CHECKOV + - REPOSITORY_DEVSKIM +YAML_YAMLLINT_CONFIG_FILE: .yamllint +REPOSITORY_KICS_ARGUMENTS: >- + scan --no-progress --exclude-severities="medium,low,info,trace" --exclude-paths="./cmd/project-api/gen/*" +SPELL_CSPELL_ANALYZE_FILE_NAMES: false +# Make sure Vale is setup to run with the styles it needs. +SPELL_VALE_PRE_COMMANDS: + - command: mkdir -p styles + cwd: "workspace" + - command: vale sync + cwd: "workspace" +API_SPECTRAL_FILTER_REGEX_EXCLUDE: "gen/" +# Ignore YAML files with templating macros; these typically fail linting and/or +# schema checking. +FILTER_REGEX_EXCLUDE: '(templates/.*\.yml|templates/.*\.yaml)' +KUBERNETES_DIRECTORY: charts/lfx-v2-project-service +KUBERNETES_HELM_ARGUMENTS: charts/lfx-v2-project-service diff --git a/.yamllint b/.yamllint new file mode 100644 index 0000000..27edb58 --- /dev/null +++ b/.yamllint @@ -0,0 +1,13 @@ +# Copyright The Linux Foundation and each contributor to LFX. +# SPDX-License-Identifier: MIT +--- +extends: default +ignore: | + .git + megalinter-reports + styles + gen/ +rules: + line-length: + max: 120 + level: warning diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..ba26d7b --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,2 @@ +# Platform engineering group within LFX engineering. +* @linuxfoundation/lfx-platform diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c725d88 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,38 @@ +# Copyright The Linux Foundation and each contributor to LFX. +# SPDX-License-Identifier: MIT + +# checkov:skip=CKV_DOCKER_7:No free access to Chainguard versioned labels. +# hadolint global ignore=DL3007 + +FROM cgr.dev/chainguard/go:latest AS builder + +# Expose port 8080 for the project service API. +EXPOSE 8080 + +# Set necessary environment variables needed for our image. Allow building to +# other architectures via cross-compilation build-arg. +ARG TARGETARCH +ENV CGO_ENABLED=0 GOOS=linux GOARCH=$TARGETARCH + +# Move to working directory /build +WORKDIR /build + +# Download dependencies to go modules cache +COPY go.mod go.sum ./ +RUN go mod download + +# Copy the code into the container +COPY . . + +# Build the packages +RUN go build -o /go/bin/project-svc -trimpath -ldflags="-w -s" github.com/linuxfoundation/lfx-v2-project-service/cmd/project-api + +# Run our go binary standalone +FROM cgr.dev/chainguard/static:latest + +# Implicit with base image; setting explicitly for linters. +USER nonroot + +COPY --from=builder /go/bin/project-svc /cmd/project-api + +ENTRYPOINT ["/cmd/project-api"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..32bf07f --- /dev/null +++ b/LICENSE @@ -0,0 +1,18 @@ +Copyright The Linux Foundation and each contributor to LFX. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/LICENSE-docs b/LICENSE-docs new file mode 100644 index 0000000..4ea99c2 --- /dev/null +++ b/LICENSE-docs @@ -0,0 +1,395 @@ +Attribution 4.0 International + +======================================================================= + +Creative Commons Corporation ("Creative Commons") is not a law firm and +does not provide legal services or legal advice. Distribution of +Creative Commons public licenses does not create a lawyer-client or +other relationship. Creative Commons makes its licenses and related +information available on an "as-is" basis. Creative Commons gives no +warranties regarding its licenses, any material licensed under their +terms and conditions, or any related information. Creative Commons +disclaims all liability for damages resulting from their use to the +fullest extent possible. + +Using Creative Commons Public Licenses + +Creative Commons public licenses provide a standard set of terms and +conditions that creators and other rights holders may use to share +original works of authorship and other material subject to copyright +and certain other rights specified in the public license below. The +following considerations are for informational purposes only, are not +exhaustive, and do not form part of our licenses. + + Considerations for licensors: Our public licenses are + intended for use by those authorized to give the public + permission to use material in ways otherwise restricted by + copyright and certain other rights. Our licenses are + irrevocable. Licensors should read and understand the terms + and conditions of the license they choose before applying it. + Licensors should also secure all rights necessary before + applying our licenses so that the public can reuse the + material as expected. Licensors should clearly mark any + material not subject to the license. This includes other CC- + licensed material, or material used under an exception or + limitation to copyright. More considerations for licensors: + wiki.creativecommons.org/Considerations_for_licensors + + Considerations for the public: By using one of our public + licenses, a licensor grants the public permission to use the + licensed material under specified terms and conditions. If + the licensor's permission is not necessary for any reason--for + example, because of any applicable exception or limitation to + copyright--then that use is not regulated by the license. Our + licenses grant only permissions under copyright and certain + other rights that a licensor has authority to grant. Use of + the licensed material may still be restricted for other + reasons, including because others have copyright or other + rights in the material. A licensor may make special requests, + such as asking that all changes be marked or described. + Although not required by our licenses, you are encouraged to + respect those requests where reasonable. More considerations + for the public: + wiki.creativecommons.org/Considerations_for_licensees + +======================================================================= + +Creative Commons Attribution 4.0 International Public License + +By exercising the Licensed Rights (defined below), You accept and agree +to be bound by the terms and conditions of this Creative Commons +Attribution 4.0 International Public License ("Public License"). To the +extent this Public License may be interpreted as a contract, You are +granted the Licensed Rights in consideration of Your acceptance of +these terms and conditions, and the Licensor grants You such rights in +consideration of benefits the Licensor receives from making the +Licensed Material available under these terms and conditions. + + +Section 1 -- Definitions. + + a. Adapted Material means material subject to Copyright and Similar + Rights that is derived from or based upon the Licensed Material + and in which the Licensed Material is translated, altered, + arranged, transformed, or otherwise modified in a manner requiring + permission under the Copyright and Similar Rights held by the + Licensor. For purposes of this Public License, where the Licensed + Material is a musical work, performance, or sound recording, + Adapted Material is always produced where the Licensed Material is + synched in timed relation with a moving image. + + b. Adapter's License means the license You apply to Your Copyright + and Similar Rights in Your contributions to Adapted Material in + accordance with the terms and conditions of this Public License. + + c. Copyright and Similar Rights means copyright and/or similar rights + closely related to copyright including, without limitation, + performance, broadcast, sound recording, and Sui Generis Database + Rights, without regard to how the rights are labeled or + categorized. For purposes of this Public License, the rights + specified in Section 2(b)(1)-(2) are not Copyright and Similar + Rights. + + d. Effective Technological Measures means those measures that, in the + absence of proper authority, may not be circumvented under laws + fulfilling obligations under Article 11 of the WIPO Copyright + Treaty adopted on December 20, 1996, and/or similar international + agreements. + + e. Exceptions and Limitations means fair use, fair dealing, and/or + any other exception or limitation to Copyright and Similar Rights + that applies to Your use of the Licensed Material. + + f. Licensed Material means the artistic or literary work, database, + or other material to which the Licensor applied this Public + License. + + g. Licensed Rights means the rights granted to You subject to the + terms and conditions of this Public License, which are limited to + all Copyright and Similar Rights that apply to Your use of the + Licensed Material and that the Licensor has authority to license. + + h. Licensor means the individual(s) or entity(ies) granting rights + under this Public License. + + i. Share means to provide material to the public by any means or + process that requires permission under the Licensed Rights, such + as reproduction, public display, public performance, distribution, + dissemination, communication, or importation, and to make material + available to the public including in ways that members of the + public may access the material from a place and at a time + individually chosen by them. + + j. Sui Generis Database Rights means rights other than copyright + resulting from Directive 96/9/EC of the European Parliament and of + the Council of 11 March 1996 on the legal protection of databases, + as amended and/or succeeded, as well as other essentially + equivalent rights anywhere in the world. + + k. You means the individual or entity exercising the Licensed Rights + under this Public License. Your has a corresponding meaning. + + +Section 2 -- Scope. + + a. License grant. + + 1. Subject to the terms and conditions of this Public License, + the Licensor hereby grants You a worldwide, royalty-free, + non-sublicensable, non-exclusive, irrevocable license to + exercise the Licensed Rights in the Licensed Material to: + + a. reproduce and Share the Licensed Material, in whole or + in part; and + + b. produce, reproduce, and Share Adapted Material. + + 2. Exceptions and Limitations. For the avoidance of doubt, where + Exceptions and Limitations apply to Your use, this Public + License does not apply, and You do not need to comply with + its terms and conditions. + + 3. Term. The term of this Public License is specified in Section + 6(a). + + 4. Media and formats; technical modifications allowed. The + Licensor authorizes You to exercise the Licensed Rights in + all media and formats whether now known or hereafter created, + and to make technical modifications necessary to do so. The + Licensor waives and/or agrees not to assert any right or + authority to forbid You from making technical modifications + necessary to exercise the Licensed Rights, including + technical modifications necessary to circumvent Effective + Technological Measures. For purposes of this Public License, + simply making modifications authorized by this Section 2(a) + (4) never produces Adapted Material. + + 5. Downstream recipients. + + a. Offer from the Licensor -- Licensed Material. Every + recipient of the Licensed Material automatically + receives an offer from the Licensor to exercise the + Licensed Rights under the terms and conditions of this + Public License. + + b. No downstream restrictions. You may not offer or impose + any additional or different terms or conditions on, or + apply any Effective Technological Measures to, the + Licensed Material if doing so restricts exercise of the + Licensed Rights by any recipient of the Licensed + Material. + + 6. No endorsement. Nothing in this Public License constitutes or + may be construed as permission to assert or imply that You + are, or that Your use of the Licensed Material is, connected + with, or sponsored, endorsed, or granted official status by, + the Licensor or others designated to receive attribution as + provided in Section 3(a)(1)(A)(i). + + b. Other rights. + + 1. Moral rights, such as the right of integrity, are not + licensed under this Public License, nor are publicity, + privacy, and/or other similar personality rights; however, to + the extent possible, the Licensor waives and/or agrees not to + assert any such rights held by the Licensor to the limited + extent necessary to allow You to exercise the Licensed + Rights, but not otherwise. + + 2. Patent and trademark rights are not licensed under this + Public License. + + 3. To the extent possible, the Licensor waives any right to + collect royalties from You for the exercise of the Licensed + Rights, whether directly or through a collecting society + under any voluntary or waivable statutory or compulsory + licensing scheme. In all other cases the Licensor expressly + reserves any right to collect such royalties. + + +Section 3 -- License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the +following conditions. + + a. Attribution. + + 1. If You Share the Licensed Material (including in modified + form), You must: + + a. retain the following if it is supplied by the Licensor + with the Licensed Material: + + i. identification of the creator(s) of the Licensed + Material and any others designated to receive + attribution, in any reasonable manner requested by + the Licensor (including by pseudonym if + designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of + warranties; + + v. a URI or hyperlink to the Licensed Material to the + extent reasonably practicable; + + b. indicate if You modified the Licensed Material and + retain an indication of any previous modifications; and + + c. indicate the Licensed Material is licensed under this + Public License, and include the text of, or the URI or + hyperlink to, this Public License. + + 2. You may satisfy the conditions in Section 3(a)(1) in any + reasonable manner based on the medium, means, and context in + which You Share the Licensed Material. For example, it may be + reasonable to satisfy the conditions by providing a URI or + hyperlink to a resource that includes the required + information. + + 3. If requested by the Licensor, You must remove any of the + information required by Section 3(a)(1)(A) to the extent + reasonably practicable. + + 4. If You Share Adapted Material You produce, the Adapter's + License You apply must not prevent recipients of the Adapted + Material from complying with this Public License. + + +Section 4 -- Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that +apply to Your use of the Licensed Material: + + a. for the avoidance of doubt, Section 2(a)(1) grants You the right + to extract, reuse, reproduce, and Share all or a substantial + portion of the contents of the database; + + b. if You include all or a substantial portion of the database + contents in a database in which You have Sui Generis Database + Rights, then the database in which You have Sui Generis Database + Rights (but not its individual contents) is Adapted Material; and + + c. You must comply with the conditions in Section 3(a) if You Share + all or a substantial portion of the contents of the database. + +For the avoidance of doubt, this Section 4 supplements and does not +replace Your obligations under this Public License where the Licensed +Rights include other Copyright and Similar Rights. + + +Section 5 -- Disclaimer of Warranties and Limitation of Liability. + + a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE + EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS + AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF + ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, + IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, + WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR + PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, + ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT + KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT + ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. + + b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE + TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, + NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, + INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, + COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR + USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN + ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR + DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR + IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. + + c. The disclaimer of warranties and limitation of liability provided + above shall be interpreted in a manner that, to the extent + possible, most closely approximates an absolute disclaimer and + waiver of all liability. + + +Section 6 -- Term and Termination. + + a. This Public License applies for the term of the Copyright and + Similar Rights licensed here. However, if You fail to comply with + this Public License, then Your rights under this Public License + terminate automatically. + + b. Where Your right to use the Licensed Material has terminated under + Section 6(a), it reinstates: + + 1. automatically as of the date the violation is cured, provided + it is cured within 30 days of Your discovery of the + violation; or + + 2. upon express reinstatement by the Licensor. + + For the avoidance of doubt, this Section 6(b) does not affect any + right the Licensor may have to seek remedies for Your violations + of this Public License. + + c. For the avoidance of doubt, the Licensor may also offer the + Licensed Material under separate terms or conditions or stop + distributing the Licensed Material at any time; however, doing so + will not terminate this Public License. + + d. Sections 1, 5, 6, 7, and 8 survive termination of this Public + License. + + +Section 7 -- Other Terms and Conditions. + + a. The Licensor shall not be bound by any additional or different + terms or conditions communicated by You unless expressly agreed. + + b. Any arrangements, understandings, or agreements regarding the + Licensed Material not stated herein are separate from and + independent of the terms and conditions of this Public License. + + +Section 8 -- Interpretation. + + a. For the avoidance of doubt, this Public License does not, and + shall not be interpreted to, reduce, limit, restrict, or impose + conditions on any use of the Licensed Material that could lawfully + be made without permission under this Public License. + + b. To the extent possible, if any provision of this Public License is + deemed unenforceable, it shall be automatically reformed to the + minimum extent necessary to make it enforceable. If the provision + cannot be reformed, it shall be severed from this Public License + without affecting the enforceability of the remaining terms and + conditions. + + c. No term or condition of this Public License will be waived and no + failure to comply consented to unless expressly agreed to by the + Licensor. + + d. Nothing in this Public License constitutes or may be interpreted + as a limitation upon, or waiver of, any privileges and immunities + that apply to the Licensor or You, including from the legal + processes of any jurisdiction or authority. + + +======================================================================= + +Creative Commons is not a party to its public +licenses. Notwithstanding, Creative Commons may elect to apply one of +its public licenses to material it publishes and in those instances +will be considered the “Licensor.” The text of the Creative Commons +public licenses is dedicated to the public domain under the CC0 Public +Domain Dedication. Except for the limited purpose of indicating that +material is shared under a Creative Commons public license or as +otherwise permitted by the Creative Commons policies published at +creativecommons.org/policies, Creative Commons does not authorize the +use of the trademark "Creative Commons" or any other trademark or logo +of Creative Commons without its prior written consent including, +without limitation, in connection with any unauthorized modifications +to any of its public licenses or any other arrangements, +understandings, or agreements concerning use of licensed material. For +the avoidance of doubt, this paragraph does not form part of the +public licenses. + +Creative Commons may be contacted at creativecommons.org. diff --git a/README.md b/README.md index 8f6894f..40746c8 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,56 @@ -# lfx-v2-project-service -LFX v2 Platform Project +# LFX V2 Project Service + +This repository contains the source code for the LFX v2 platform project service. + +## Overview + +The LFX v2 Project Service is a RESTful API service that manages projects within the Linux Foundation's LFX platform. It provides endpoints for creating, reading, updating, and deleting projects with built-in authorization and audit capabilities. + +## File Structure + +```bash +├── .github/ # Github files +│ └── workflows/ # Github Action workflow files +├── charts/ # Helm charts for running the service in kubernetes +├── cmd/ # Services (main packages) +│ └── project-api/ # Project service code +│ ├── gen/ # Generated code from Goa design +│ └── design/ # API design specifications +├── internal/ # Internal service packages +│ ├── domain/ # Domain logic layer +│ ├── service/ # Business logic layer +│ ├── middleware/ # HTTP middleware components +│ └── infrastructure/ # Infrastructure layer +│ └── nats/ # NATS messaging infrastructure +└── pkg/ # Shared packages + └── constants/ # Shared constants and configurations +``` + +## Key Features + +- **RESTful API**: Full CRUD operations for project management +- **NATS Integration**: Event-driven architecture using NATS for messaging and key-value storage +- **Authorization**: JWT-based authentication with Heimdall middleware integration +- **OpenFGA Support**: Fine-grained authorization control (configurable) +- **Health Checks**: Built-in `/livez` and `/readyz` endpoints +- **Request Tracking**: Automatic request ID generation and propagation +- **Structured Logging**: JSON-formatted logs with contextual information + +## Contributing + +To contribute to this repository: + +1. Fork the repository +2. Make your changes +3. Submit a pull request + +## License + +Copyright The Linux Foundation and each contributor to LFX. + +This project’s source code is licensed under the MIT License. A copy of the +license is available in `LICENSE`. + +This project’s documentation is licensed under the Creative Commons Attribution +4.0 International License \(CC-BY-4.0\). A copy of the license is available in +`LICENSE-docs`. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..5b2bddd --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,9 @@ +# Security policy + +## Reporting a security vulnerability + +To report a security vulnerability, please use GitHub's feature for private +security reporting. + +For more information, please read: [How to report vulnerabilities to LF projects +and foundations](https://www.linuxfoundation.org/security). diff --git a/charts/lfx-v2-project-service/Chart.yaml b/charts/lfx-v2-project-service/Chart.yaml new file mode 100644 index 0000000..1e4b906 --- /dev/null +++ b/charts/lfx-v2-project-service/Chart.yaml @@ -0,0 +1,9 @@ +# Copyright The Linux Foundation and each contributor to LFX. +# SPDX-License-Identifier: MIT +--- +apiVersion: v2 +name: lfx-v2-project-service +description: LFX Platform V2 Project Service chart +type: application +version: 0.1.0 +appVersion: "0.1.0" diff --git a/charts/lfx-v2-project-service/templates/deployment.yaml b/charts/lfx-v2-project-service/templates/deployment.yaml new file mode 100644 index 0000000..d8e246e --- /dev/null +++ b/charts/lfx-v2-project-service/templates/deployment.yaml @@ -0,0 +1,48 @@ +# Copyright The Linux Foundation and each contributor to LFX. +# SPDX-License-Identifier: MIT +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: lfx-v2-project-service + namespace: lfx +spec: + replicas: 1 + selector: + matchLabels: + app: lfx-v2-project-service + template: + metadata: + labels: + app: lfx-v2-project-service + spec: + containers: + - name: app + image: linuxfoundation/lfx-v2-project-service:{{ .Chart.AppVersion }} + securityContext: + allowPrivilegeEscalation: false + imagePullPolicy: Never # The image is expected to exist locally with the same name and tag. It is not deployed to a registry. + env: + - name: NATS_URL + value: {{.Values.nats.url}} + ports: + - containerPort: 8080 + name: web + livenessProbe: + httpGet: + path: /livez + port: web + failureThreshold: 3 + periodSeconds: 15 + readinessProbe: + httpGet: + path: /readyz + port: web + failureThreshold: 1 + periodSeconds: 10 + startupProbe: + httpGet: + path: /readyz + port: web + failureThreshold: 30 + periodSeconds: 1 diff --git a/charts/lfx-v2-project-service/templates/heimdall.yaml b/charts/lfx-v2-project-service/templates/heimdall.yaml new file mode 100644 index 0000000..265e1d4 --- /dev/null +++ b/charts/lfx-v2-project-service/templates/heimdall.yaml @@ -0,0 +1,13 @@ +# Copyright The Linux Foundation and each contributor to LFX. +# SPDX-License-Identifier: MIT +--- +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: lfx-v2-project-service + namespace: lfx +spec: + forwardAuth: + address: {{ .Values.heimdall.url }} + authResponseHeaders: + - Authorization diff --git a/charts/lfx-v2-project-service/templates/ingressroute.yaml b/charts/lfx-v2-project-service/templates/ingressroute.yaml new file mode 100644 index 0000000..11fc938 --- /dev/null +++ b/charts/lfx-v2-project-service/templates/ingressroute.yaml @@ -0,0 +1,28 @@ +# Copyright The Linux Foundation and each contributor to LFX. +# SPDX-License-Identifier: MIT +--- +# TODO: use the newer Gateway API instead of a IngressRoute resource +# https://doc.traefik.io/traefik/routing/providers/kubernetes-gateway/ +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: lfx-v2-project-service + namespace: lfx +spec: + entryPoints: + - web + - websecure + routes: + - kind: Rule + match: >- + Host(`{{.Values.ingress.hostname}}`) && + (Path(`/projects`) || PathPrefix(`/projects/`) || Path(`/livez`) || Path(`/readyz`)) + priority: 10 + middlewares: + {{- if .Values.heimdall.enabled }} + - name: heimdall + {{- end }} + services: + - kind: Service + name: lfx-v2-project-service + port: web diff --git a/charts/lfx-v2-project-service/templates/nats-kv-bucket.yaml b/charts/lfx-v2-project-service/templates/nats-kv-bucket.yaml new file mode 100644 index 0000000..8f6f74a --- /dev/null +++ b/charts/lfx-v2-project-service/templates/nats-kv-bucket.yaml @@ -0,0 +1,21 @@ +# Copyright The Linux Foundation and each contributor to LFX. +# SPDX-License-Identifier: MIT +--- +{{- if .Values.nats.projects_kv_bucket.creation }} +apiVersion: jetstream.nats.io/v1beta2 +kind: KeyValue +metadata: + name: {{ .Values.nats.projects_kv_bucket.name }} + namespace: lfx + {{- if .Values.nats.projects_kv_bucket.keep }} + annotations: + "helm.sh/resource-policy": keep + {{- end }} +spec: + bucket: {{ .Values.nats.projects_kv_bucket.name }} + history: {{ .Values.nats.projects_kv_bucket.history }} + storage: {{ .Values.nats.projects_kv_bucket.storage }} + maxValueSize: {{ .Values.nats.projects_kv_bucket.maxValueSize }} + maxBytes: {{ .Values.nats.projects_kv_bucket.maxBytes }} + compression: {{ .Values.nats.projects_kv_bucket.compression }} +{{- end }} diff --git a/charts/lfx-v2-project-service/templates/ruleset.yaml b/charts/lfx-v2-project-service/templates/ruleset.yaml new file mode 100644 index 0000000..d6661ea --- /dev/null +++ b/charts/lfx-v2-project-service/templates/ruleset.yaml @@ -0,0 +1,91 @@ +# Copyright The Linux Foundation and each contributor to LFX. +# SPDX-License-Identifier: MIT +--- +apiVersion: heimdall.dadrus.github.com/v1alpha4 +kind: RuleSet +metadata: + name: lfx-v2-project-service + namespace: lfx +spec: + rules: + - id: "rule:lfx:lfx-v2-project-service:health" + match: + methods: + - GET + routes: + - path: /livez + - path: /readyz + execute: + - authenticator: anonymous_authenticator + - authorizer: allow_all + - finalizer: create_jwt + config: + values: + aud: lfx-v2-project-service + - id: "rule:lfx:lfx-v2-project-service:projects" + match: + methods: + - GET + - POST + routes: + - path: /projects + execute: + #- authenticator: authelia + - authenticator: anonymous_authenticator + #- contextualizer: authelia_userinfo + - authorizer: allow_all + - finalizer: create_jwt + config: + values: + aud: lfx-v2-project-service + - id: "rule:lfx:lfx-v2-project-service:projects:get_single" + match: + methods: + - GET + routes: + - path: /projects/:id + execute: + #- authenticator: authelia + - authenticator: anonymous_authenticator + #- contextualizer: authelia_userinfo + {{- if .Values.openfga.enabled }} + - authorizer: openfga_check + config: + values: + relation: viewer + object: "project:{{ "{{- .Request.URL.Captures.id -}}" }}" + {{- else -}} + # When OpenFGA is disabled, allow all requests + # (Only meant for *local development* because OpenFGA should be enabled when deployed) + - authorizer: allow_all + {{- end }} + - finalizer: create_jwt + config: + values: + aud: lfx-v2-project-service + - id: "rule:lfx:lfx-v2-project-service:projects:single_project_write" + match: + methods: + - PUT + - DELETE + routes: + - path: /projects/:id + execute: + #- authenticator: authelia + - authenticator: anonymous_authenticator + #- contextualizer: authelia_userinfo + {{- if .Values.openfga.enabled }} + - authorizer: openfga_check + config: + values: + relation: writer + object: "project:{{ "{{- .Request.URL.Captures.id -}}" }}" + {{- else -}} + # When OpenFGA is disabled, allow all requests + # (Only meant for *local development* because OpenFGA should be enabled when deployed) + - authorizer: allow_all + {{- end }} + - finalizer: create_jwt + config: + values: + aud: lfx-v2-project-service diff --git a/charts/lfx-v2-project-service/templates/service.yaml b/charts/lfx-v2-project-service/templates/service.yaml new file mode 100644 index 0000000..985082c --- /dev/null +++ b/charts/lfx-v2-project-service/templates/service.yaml @@ -0,0 +1,17 @@ +# Copyright The Linux Foundation and each contributor to LFX. +# SPDX-License-Identifier: MIT +--- +apiVersion: v1 +kind: Service +metadata: + name: lfx-v2-project-service + namespace: lfx + +spec: + ports: + - name: web + port: 8080 + targetPort: web + + selector: + app: lfx-v2-project-service diff --git a/charts/lfx-v2-project-service/values.yaml b/charts/lfx-v2-project-service/values.yaml new file mode 100644 index 0000000..c5e1e3a --- /dev/null +++ b/charts/lfx-v2-project-service/values.yaml @@ -0,0 +1,45 @@ +# Copyright The Linux Foundation and each contributor to LFX. +# SPDX-License-Identifier: MIT +--- +# ingress is the configuration for the ingress routing +ingress: + # hostname is the hostname of the ingress + hostname: lfx-api.k8s.orb.local + +# nats is the configuration for the NATS server +nats: + # url is the URL of the NATS server + url: nats://lfx-platform-nats.lfx.svc.cluster.local:4222 + + # projects_kv_bucket is the configuration for the KV bucket for storing projects + projects_kv_bucket: + # creation is a boolean to determine if the KV bucket should be created via the helm chart. + # set it to false if you want to use an existing KV bucket. + creation: true + # keep is a boolean to determine if the KV bucket should be preserved during helm uninstall + # set it to false if you want the bucket to be deleted when the chart is uninstalled + keep: true + # name is the name of the KV bucket for storing projects + name: projects + # history is the number of history entries to keep for the KV bucket + history: 20 + # storage is the storage type for the KV bucket + storage: file + # maxValueSize is the maximum size of a value in the KV bucket + maxValueSize: 10485760 # 10MB + # maxBytes is the maximum number of bytes in the KV bucket + maxBytes: 1073741824 # 1GB + # compression is a boolean to determine if the KV bucket should be compressed + compression: true + +# openfga is the configuration for the OpenFGA server +openfga: + # enabled is a boolean to determine if the OpenFGA server should be enabled for authorization + # Note: If it is disabled, then the project service will allow all requests + # (Disabling OpenFGA should only be used for local development). + enabled: true + +# heimdall is the configuration for the heimdall middleware +heimdall: + enabled: true + url: http://heimdall.lfx.svc.cluster.local:4456 diff --git a/cmd/project-api/Makefile b/cmd/project-api/Makefile new file mode 100644 index 0000000..85bf883 --- /dev/null +++ b/cmd/project-api/Makefile @@ -0,0 +1,191 @@ +# Copyright The Linux Foundation and each contributor to LFX. +# SPDX-License-Identifier: MIT + +# Variables +BINARY_NAME=project-api +BINARY_PATH=bin/$(BINARY_NAME) +GO_MODULE=github.com/linuxfoundation/lfx-v2-project-service +CMD_PATH=$(GO_MODULE)/cmd/project-api +DESIGN_MODULE=$(CMD_PATH)/design +GO_FILES=$(shell find . -name '*.go' -not -path './gen/*' -not -path './vendor/*') +GOA_VERSION=v3 + +# Docker variables +DOCKER_IMAGE=linuxfoundation/lfx-v2-project-service +DOCKER_TAG=0.1.0 + +# Helm variables +HELM_CHART_PATH=../../charts/lfx-v2-project-service +HELM_RELEASE_NAME=lfx-v2-project-service +HELM_NAMESPACE=lfx + +# Build variables +BUILD_TIME=$(shell date -u '+%Y-%m-%d_%H:%M:%S') +GIT_COMMIT=$(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown") +VERSION=$(shell git describe --tags --always --dirty 2>/dev/null || echo "dev") +LDFLAGS=-ldflags "-X main.Version=$(VERSION) -X main.BuildTime=$(BUILD_TIME) -X main.GitCommit=$(GIT_COMMIT)" + +# Test variables +TEST_FLAGS=-v -race -cover +TEST_TIMEOUT=5m + +.PHONY: all help deps apigen build run debug test test-verbose test-coverage test-integration test-authorization clean lint fmt check verify docker helm-install helm-uninstall + +# Default target +all: clean deps apigen fmt lint test build + +# Help target +help: + @echo "Available targets:" + @echo " all - Run clean, deps, apigen, fmt, lint, test, and build" + @echo " deps - Install dependencies including goa CLI" + @echo " apigen - Generate API code from design files" + @echo " build - Build the binary" + @echo " run - Run the service" + @echo " debug - Run the service with debug logging" + @echo " test - Run unit tests" + @echo " test-verbose - Run tests with verbose output" + @echo " test-coverage - Run tests with coverage report" + @echo " test-integration - Run integration tests (requires build tags)" + @echo " test-authorization - Run authorization integration test script" + @echo " clean - Remove generated files and binaries" + @echo " lint - Run golangci-lint" + @echo " fmt - Format Go code" + @echo " check - Run fmt and lint without modifying files" + @echo " verify - Verify API generation is up to date" + @echo " docker - Build Docker image" + @echo " helm-install - Install Helm chart" + @echo " helm-uninstall - Uninstall Helm chart" + +# Install dependencies +deps: + @echo "==> Installing dependencies..." + go mod download + go install goa.design/goa/$(GOA_VERSION)/cmd/goa@latest + @command -v golangci-lint >/dev/null 2>&1 || { \ + echo "==> Installing golangci-lint..."; \ + go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest; \ + } + +# Generate API code from design files +apigen: deps + @echo "==> Generating API code..." + goa gen $(DESIGN_MODULE) + @echo "==> API generation complete" + +# Build the binary +build: clean + @echo "==> Building $(BINARY_NAME)..." + @mkdir -p bin + go build $(LDFLAGS) -o $(BINARY_PATH) . + @echo "==> Build complete: $(BINARY_PATH)" + +# Run the service +run: apigen + @echo "==> Running $(BINARY_NAME)..." + go run $(LDFLAGS) . + +# Run with debug logging +debug: apigen + @echo "==> Running $(BINARY_NAME) in debug mode..." + go run $(LDFLAGS) . -d + +# Run tests +test: + @echo "==> Running tests..." + go test $(TEST_FLAGS) -timeout $(TEST_TIMEOUT) ./... + +# Run tests with verbose output +test-verbose: + @echo "==> Running tests (verbose)..." + go test $(TEST_FLAGS) -v -timeout $(TEST_TIMEOUT) ./... + +# Run tests with coverage +test-coverage: + @echo "==> Running tests with coverage..." + @mkdir -p coverage + go test $(TEST_FLAGS) -timeout $(TEST_TIMEOUT) -coverprofile=coverage/coverage.out ./... + go tool cover -html=coverage/coverage.out -o coverage/coverage.html + @echo "==> Coverage report: coverage/coverage.html" + +# Run integration tests +test-integration: + @echo "==> Running integration tests..." + go test $(TEST_FLAGS) -timeout $(TEST_TIMEOUT) -tags=integration ./... + +# Run authorization integration test script +test-authorization: + @echo "==> Running authorization integration tests..." + @if [ ! -f ../../test/authorization_test.sh ]; then \ + echo "Authorization test script not found at ../../test/authorization_test.sh"; \ + exit 1; \ + fi + @../../test/authorization_test.sh + +# Clean build artifacts +clean: + @echo "==> Cleaning build artifacts..." + @rm -rf bin/ coverage/ + @go clean -cache + @echo "==> Clean complete" + +# Run linter +lint: + @echo "==> Running linter..." + @if command -v golangci-lint >/dev/null 2>&1; then \ + golangci-lint run ./...; \ + else \ + echo "golangci-lint not found. Run 'make deps' to install it."; \ + exit 1; \ + fi + +# Format code +fmt: + @echo "==> Formatting code..." + @go fmt ./... + @gofmt -s -w $(GO_FILES) + +# Check formatting and linting without modifying files +check: + @echo "==> Checking code format..." + @if [ -n "$$(gofmt -l $(GO_FILES))" ]; then \ + echo "The following files need formatting:"; \ + gofmt -l $(GO_FILES); \ + exit 1; \ + fi + @echo "==> Code format check passed" + @$(MAKE) lint + +# Verify that generated code is up to date +verify: apigen + @echo "==> Verifying generated code is up to date..." + @if [ -n "$$(git status --porcelain gen/)" ]; then \ + echo "Generated code is out of date. Run 'make apigen' and commit the changes."; \ + git status --porcelain gen/; \ + exit 1; \ + fi + @echo "==> Generated code is up to date" + +# Build Docker image +docker: + @echo "==> Building Docker image..." + docker build -t $(DOCKER_IMAGE):$(DOCKER_TAG) -f ../../Dockerfile ../../ + @echo "==> Docker image built: $(DOCKER_IMAGE):$(DOCKER_TAG)" + +# Install Helm chart +helm-install: + @echo "==> Installing Helm chart..." + helm upgrade --install $(HELM_RELEASE_NAME) $(HELM_CHART_PATH) --namespace $(HELM_NAMESPACE) + @echo "==> Helm chart installed: $(HELM_RELEASE_NAME)" + +# Print templates for Helm chart +helm-templates: + @echo "==> Printing templates for Helm chart..." + helm template $(HELM_RELEASE_NAME) $(HELM_CHART_PATH) --namespace $(HELM_NAMESPACE) + @echo "==> Templates printed for Helm chart: $(HELM_RELEASE_NAME)" + +# Uninstall Helm chart +helm-uninstall: + @echo "==> Uninstalling Helm chart..." + helm uninstall $(HELM_RELEASE_NAME) --namespace $(HELM_NAMESPACE) + @echo "==> Helm chart uninstalled: $(HELM_RELEASE_NAME)" diff --git a/cmd/project-api/README.md b/cmd/project-api/README.md new file mode 100644 index 0000000..b6fd532 --- /dev/null +++ b/cmd/project-api/README.md @@ -0,0 +1,236 @@ +# Project API + +This directory contains the Project API service. The service does a couple of things: + +- It serves HTTP requests via Traefik to perform CRUD operations on project data +- It listens on a NATS connection for messages from external services to also perform operations on project data + +Applications with a BFF should use the REST API with HTTP requests to perform the needed operations on projects, while other resource API services should communicate with this service via NATS messages. + +This service contains the following API endpoints: + +- `/readyz`: + - `GET`: checks that the service is able to take in inbound requests +- `/livez`: + - `GET`: checks that the service is alive +- `/projects` + - `GET`: fetch the list of projects (Note: this will be removed in favor of using the query service, once implemented) + - `POST` create a new project +- `/projects/:id` + - `GET`: fetch a project by its UID + - `PUT`: update a project by its UID - only certain attributes can be updated, read the openapi spec for more details + - `DELETE`: delete a project by its UID + +This service handles the following NATS subjects: + +- `.lfx.projects-api.get_name`: Get a project name from a given project UID +- `.lfx.projects-api.slug_to_uid`: Get a project UID from a given project slug + +## File Structure + +```bash +├── design/ # Goa design files +│ ├── project.go # Goa project service +│ └── types.go # Goa models +├── gen/ # Goa generated implementation code (not committed) +├── main.go # Dependency injection and startup +├── service.go # Base service implementation +├── service_endpoint.go # Service implementation of health check endpoints +├── service_endpoint_project.go # Service implementation of project REST API endpoints +├── service_handler.go # Service implementation of NATS handlers +├── repo.go # Interface with data stores +├── mock.go # Service mocks for tests +└── jwt.go # API authentication with Heimdall +``` + +## Development + +### Prerequisites + +- [**Go**](https://go.dev/): the service is built with the Go programming language [[Install](https://go.dev/doc/install)] +- [**Kubernetes**](https://kubernetes.io/): used for deployment of resources [[Install](https://kubernetes.io/releases/download/)] +- [**Helm**](https://helm.sh/): used to manage kubernetes applications [[Install](https://helm.sh/docs/intro/install/)] +- [**NATS**](https://docs.nats.io/): used to communicate with other LFX V2 services [[Install](https://docs.nats.io/running-a-nats-service/introduction/installation)] +- [**GOA Framework**](https://goa.design/): used for API code generation + +#### GOA Framework + +Follow the [GOA installation guide](https://goa.design/docs/2-getting-started/1-installation/) to install GOA: + +```bash +go install goa.design/goa/v3/cmd/goa@latest +``` + +Verify the installation: + +```bash +goa version +``` + +### Building and Development + +#### 1. Generate Code + +The service uses GOA to generate API code from the design specification. Run the following command to generate all necessary code: + +```bash +make apigen + +# or directly run the "goa gen" command +goa gen github.com/linuxfoundation/lfx-v2-project-service/cmd/project-api/design +``` + +This command generates: + +- HTTP server and client code +- OpenAPI specification +- Service interfaces and types +- Transport layer implementations + +#### 2. Set up resources and external services + +The service relies on some resources and external services being spun up prior to running this service. + +- [NATS service](https://docs.nats.io/): ensure you have a NATS server instance running and set the `NATS_URL` environment variable with the URL of the server + + ```bash + export NATS_URL=nats://lfx-platform-nats.lfx.svc.cluster.local:4222 + ``` + +- [NATS key-value bucket](https://docs.nats.io/nats-concepts/jetstream/key-value-store): once you have a NATS service running, you need to create a bucket used by the project service. + + ```bash + # if using the nats cli tool + nats kv add projects --history=20 --storage=file --max-value-size=10485760 --max-bucket-size=1073741824 + ``` + +#### 3. Export environment variables + +|Environment Variable Name|Description|Default|Required| +|-----------------------|--------------------|-----------|-----| +|PORT|the port for http requests to the project service API|8080|false| +|NATS_URL|the URL of the nats server instance|nats://localhost:4222|false| +|LFX_ENVIRONMENT|the LFX environment (enum: prod, stg, dev)|dev|false| +|LOG_LEVEL|the log level for outputted logs|info|false| +|LOG_ADD_SOURCE|whether to add the source field to outputted logs|false|false| +|JWKS_URL|the URL to the endpoint for verifying ID tokens and JWT access tokens||false| +|AUDIENCE|the audience of the app that the JWT token should have set - for verification of the JWT token|lfx-v2-project-service|false| +|JWT_AUTH_DISABLED_MOCK_LOCAL_PRINCIPAL|a mocked auth principal value for local development (to avoid needing a valid JWT token)||false| + +#### 4. Development Workflow + +1. **Make design or implementation changes**: Edit files in the `design/` directory for design changes, and edit the other files for implementation changes. + +2. **Regenerate code**: Run `make apigen` after design changes + +3. **Build the service**: + + ```bash + make build + ``` + +4. **Run the service**: + + ```bash + make run + + # or run with debug logs enabled + make debug + + # or run with the go command to set custom flags + # -bind string interface to bind on (default "*") + # -d enable debug logging (default false) + # -p string listen port (default "8080") + go run + ``` + + Once the service is running, make a request to the `/livez` endpoint to ensure that the service is alive. + + ```bash + curl http://localhost:8080/livez + ``` + + You should get a 200 status code response with a text/plain content payload of `OK`. + +5. **Run tests**: + + ```bash + make test + + # or run go test to set custom flags + go test . -v + ``` + +6. **Lint the code** + + ```bash + # From the root of the directory, run megalinter (https://megalinter.io/latest/mega-linter-runner/) to ensure the code passes the linter checks. The CI/CD has a check that uses megalinter. + npx mega-linter-runner . + ``` + +7. **Docker build + K8** + + ```bash + # Build the dockerfile (from the root of the repo) + docker build -t lfx-v2-project-service: . + + # Install the helm chart for the service into the lfx namespace (from the root of the repo) + helm install lfx-v2-project-service ./charts/lfx-v2-project-service/ -n lfx + + # Once you have already installed the helm chart and need to just update it, use the following command (from the root of the repo): + helm upgrade lfx-v2-project-service ./charts/lfx-v2-project-service/ -n lfx + + # Check that the REST API is accessible by hitting the `/livez` endpoint (you should get a response of OK if it is working): + # + # Note: replace the hostname with the host from ./charts/lfx-v2-project-service/ingressroute.yaml + curl http://lfx-api.k8s.orb.local/livez + ``` + +### Authorization with OpenFGA + +When deployed via Kubernetes, the project service uses OpenFGA for fine-grained authorization control. The authorization is handled by Heimdall middleware before requests reach the service. + +#### Configuration + +OpenFGA authorization is controlled by the `openfga.enabled` value in the Helm chart: + +```yaml +# In values.yaml or via --set flag +openfga: + enabled: true # Enable OpenFGA authorization (default) + # enabled: false # Disable for local development only +``` + +#### Authorization Rules + +When OpenFGA is enabled, the following authorization checks are enforced: + +- **GET /projects** - No OpenFGA check (returns list of all projects) +- **POST /projects** - No OpenFGA check (authenticated users can create projects) +- **GET /projects/:id** - Requires `viewer` relation on the specific project +- **PUT /projects/:id** and **DELETE /projects/:id** - Requires `writer` relation on the specific project + +#### Local Development + +For local development without OpenFGA: + +1. Set `openfga.enabled: false` in your Helm values +2. All requests will be allowed through (after JWT authentication) +3. **Warning**: Never disable OpenFGA in production environments + +### Add new API endpoints + +Note: follow the [Development Workflow](#4-development-workflow) section on how to run the service code + +1. **Update design files**: Edit project file in `design/` to include specification of the new endpoint with all of its supported parameters, responses, and errors, etc. +2. **Regenerate code**: Run `make apigen` after design changes +3. **Implement code**: Implement the new endpoint in `service_endpoint_project.go` (for project-related endpoints) or create a new `service_endpoint_*.go` file for other resource types. Follow similar standards of the other endpoint methods. Include tests for the new endpoint in the corresponding `*_test.go` file. +4. **Update heimdall ruleset**: Ensure that `/charts/lfx-v2-project-service/templates/ruleset.yaml` has the route and method for the endpoint set so that authentication is configured when deployed. If the endpoint modifies data (PUT, DELETE, PATCH), consider adding OpenFGA authorization checks in the ruleset for proper access control + +### Add new message handlers + +Note: follow the [Development Workflow](#4-development-workflow) section on how to run the service code + +1. **Update main.go**: In `main.go` is the code for subscribing the service to specific NATS queue subjects. Add the subscription code in the `createNatsSubcriptions` function. If a new subject needs to be subscribed, add the subject to the `../pkg/constants` directory in a similiar fashion as the other subject names (so that it can be referenced by other services that need to send messages for the subject). +2. **Update service_handler.go**: Implement the NATS message handler. Add a new function, such as `HandleProjectGetName` for handling messages with respect to getting the name of a project. The `HandleNatsMessage` function switch statement should also be updated to include the new subject and function call. +3. **Update service_handler_test.go**: Add unit tests for the new handler function. Mock external service calls so that the tests are modular. diff --git a/cmd/project-api/design/project.go b/cmd/project-api/design/project.go new file mode 100644 index 0000000..65a1a41 --- /dev/null +++ b/cmd/project-api/design/project.go @@ -0,0 +1,269 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +// Package design contains the DSL for the project service Goa API generation. +package design + +import ( + //nolint:staticcheck // ST1001: the recommended way of using the goa GSL package is with the . import + . "goa.design/goa/v3/dsl" +) + +// JWTAuth is the DSL JWT security type for authentication. +var JWTAuth = JWTSecurity("jwt", func() { + Description("Heimdall authorization") +}) + +var _ = Service("project-service", func() { + Description("The project service provides LFX Project resources.") + + // TODO: delete this endpoint once the query service is implemented + Method("get-projects", func() { + Description("Get all projects.") + + Security(JWTAuth) + + Payload(func() { + Token("bearer_token", String, func() { + Description("JWT token issued by Heimdall") + Example("eyJhbGci...") + }) + Attribute("version", String, "Version of the API", func() { + Enum("1") + Example("1") + }) + }) + + Result(func() { + Attribute("projects", ArrayOf(Project), "Resources found", func() {}) + Attribute("cache_control", String, "Cache control header", func() { + Example("public, max-age=300") + }) + Required("projects") + }) + + Error("BadRequest", BadRequestError, "Bad request") + Error("InternalServerError", InternalServerError, "Internal server error") + Error("ServiceUnavailable", ServiceUnavailableError, "Service unavailable") + + HTTP(func() { + GET("/projects") + Param("version:v") + Header("bearer_token:Authorization") + Response(StatusOK, func() { + Header("cache_control:Cache-Control") + }) + Response("BadRequest", StatusBadRequest) + Response("InternalServerError", StatusInternalServerError) + Response("ServiceUnavailable", StatusServiceUnavailable) + }) + }) + + Method("create-project", func() { + Description("Create a new project.") + + Security(JWTAuth) + + Payload(func() { + Token("bearer_token", String, func() { + Description("JWT token issued by Heimdall") + Example("eyJhbGci...") + }) + Attribute("version", String, "Version of the API", func() { + Enum("1") + Example("1") + }) + ProjectSlugAttribute() + ProjectDescriptionAttribute() + ProjectNameAttribute() + ProjectPublicAttribute() + ProjectParentUIDAttribute() + ProjectAuditorsAttribute() + ProjectWritersAttribute() + Required("slug", "description", "name") + }) + + Result(Project) + + Error("BadRequest", BadRequestError, "Bad request") + Error("Conflict", ConflictError, "Conflict") + Error("InternalServerError", InternalServerError, "Internal server error") + Error("ServiceUnavailable", ServiceUnavailableError, "Service unavailable") + + HTTP(func() { + POST("/projects") + Param("version:v") + Header("bearer_token:Authorization") + Response(StatusCreated) + Response("BadRequest", StatusBadRequest) + Response("Conflict", StatusConflict) + Response("InternalServerError", StatusInternalServerError) + Response("ServiceUnavailable", StatusServiceUnavailable) + }) + }) + + Method("get-one-project", func() { + Description("Get a single project.") + + Security(JWTAuth) + + Payload(func() { + Token("bearer_token", String, func() { + Description("JWT token issued by Heimdall") + Example("eyJhbGci...") + }) + Attribute("version", String, "Version of the API", func() { + Enum("1") + Example("1") + }) + ProjectIDAttribute() + }) + + Result(func() { + Attribute("project", Project) + Attribute("etag", String, "ETag header value") + Required("project") + }) + + Error("NotFound", NotFoundError, "Resource not found") + Error("InternalServerError", InternalServerError, "Internal server error") + Error("ServiceUnavailable", ServiceUnavailableError, "Service unavailable") + + HTTP(func() { + GET("/projects/{id}") + Param("version:v") + Param("id") + Header("bearer_token:Authorization") + Response(StatusOK, func() { + Body("project") + Header("etag:ETag") + }) + Response("NotFound", StatusNotFound) + Response("InternalServerError", StatusInternalServerError) + Response("ServiceUnavailable", StatusServiceUnavailable) + }) + }) + + Method("update-project", func() { + Description("Update an existing project.") + + Security(JWTAuth) + + Payload(func() { + Token("bearer_token", String, func() { + Description("JWT token issued by Heimdall") + Example("eyJhbGci...") + }) + Attribute("etag", String, "ETag header value", func() { + Example("123") + }) + Attribute("version", String, "Version of the API", func() { + Enum("1") + Example("1") + }) + ProjectIDAttribute() + ProjectSlugAttribute() + ProjectDescriptionAttribute() + ProjectNameAttribute() + ProjectPublicAttribute() + ProjectParentUIDAttribute() + ProjectAuditorsAttribute() + ProjectWritersAttribute() + Required("slug", "description", "name") + }) + + Result(Project) + + Error("BadRequest", BadRequestError, "Bad request") + Error("NotFound", NotFoundError, "Resource not found") + Error("InternalServerError", InternalServerError, "Internal server error") + Error("ServiceUnavailable", ServiceUnavailableError, "Service unavailable") + + HTTP(func() { + PUT("/projects/{id}") + Params(func() { + Param("version:v") + Param("id") + }) + Header("bearer_token:Authorization") + Header("etag:ETag") + Response(StatusOK) + Response("BadRequest", StatusBadRequest) + Response("NotFound", StatusNotFound) + Response("InternalServerError", StatusInternalServerError) + Response("ServiceUnavailable", StatusServiceUnavailable) + }) + }) + + Method("delete-project", func() { + Description("Delete an existing project.") + + Security(JWTAuth) + + Payload(func() { + Token("bearer_token", String, func() { + Description("JWT token issued by Heimdall") + Example("eyJhbGci...") + }) + Attribute("etag", String, "ETag header value", func() { + Example("123") + }) + Attribute("version", String, "Version of the API", func() { + Enum("1") + Example("1") + }) + ProjectIDAttribute() + }) + + Error("NotFound", NotFoundError, "Resource not found") + Error("BadRequest", BadRequestError, "Bad request") + Error("InternalServerError", InternalServerError, "Internal server error") + Error("ServiceUnavailable", ServiceUnavailableError, "Service unavailable") + + HTTP(func() { + DELETE("/projects/{id}") + Params(func() { + Param("version:v") + Param("id") + }) + Header("bearer_token:Authorization") + Header("etag:ETag") + Response(StatusNoContent) + Response("NotFound", StatusNotFound) + Response("BadRequest", StatusBadRequest) + Response("InternalServerError", StatusInternalServerError) + Response("ServiceUnavailable", StatusServiceUnavailable) + }) + }) + + Method("readyz", func() { + Description("Check if the service is able to take inbound requests.") + Result(Bytes, func() { + Example("OK") + }) + Error("ServiceUnavailable", ServiceUnavailableError, "Service is unavailable") + HTTP(func() { + GET("/readyz") + Response(StatusOK, func() { + ContentType("text/plain") + }) + Response("ServiceUnavailable", StatusServiceUnavailable) + }) + }) + + Method("livez", func() { + Description("Check if the service is alive.") + Result(Bytes, func() { + Example("OK") + }) + HTTP(func() { + GET("/livez") + Response(StatusOK, func() { + ContentType("text/plain") + }) + }) + }) + + // Serve the file gen/http/openapi3.json for requests sent to /openapi.json. + Files("/openapi.json", "gen/http/openapi3.json") +}) diff --git a/cmd/project-api/design/types.go b/cmd/project-api/design/types.go new file mode 100644 index 0000000..3450229 --- /dev/null +++ b/cmd/project-api/design/types.go @@ -0,0 +1,148 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +package design + +import ( + //nolint:staticcheck // ST1001: the recommended way of using the goa GSL package is with the . import + . "goa.design/goa/v3/dsl" +) + +// Project is the DSL type for a project. +var Project = Type("Project", func() { + Description("A representation of LFX Projects.") + + // Attributes + ProjectIDAttribute() + ProjectSlugAttribute() + ProjectDescriptionAttribute() + ProjectNameAttribute() + ProjectPublicAttribute() + ProjectParentUIDAttribute() + ProjectAuditorsAttribute() + ProjectWritersAttribute() +}) + +// +// Project attributes +// + +// ProjectIDAttribute is the DSL attribute for a project ID. +func ProjectIDAttribute() { + Attribute("id", String, "Project ID -- v2 id, not related to v1 id directly", func() { + Example("7cad5a8d-19d0-41a4-81a6-043453daf9ee") + Format(FormatUUID) + }) +} + +// ProjectSlugAttribute is the DSL attribute for a project slug. +func ProjectSlugAttribute() { + Attribute("slug", String, "Project slug, a short slugified name of the project", func() { + Example("project-slug") + Format(FormatRegexp) + Pattern(`^[a-z][a-z0-9_\-]*[a-z0-9]$`) + }) +} + +// ProjectNameAttribute is the DSL attribute for a project name. +func ProjectNameAttribute() { + Attribute("name", String, "The pretty name of the project", func() { + Example("Foo Foundation") + }) +} + +// ProjectDescriptionAttribute is the DSL attribute for a project description. +func ProjectDescriptionAttribute() { + Attribute("description", String, "A description of the project", func() { + Example("project foo is a project about bar") + }) +} + +// ProjectPublicAttribute is the DSL attribute for a project public flag. +func ProjectPublicAttribute() { + Attribute("public", Boolean, "Whether the project is public", func() { + Example(true) + }) +} + +// ProjectParentUIDAttribute is the DSL attribute for a project parent UID. +func ProjectParentUIDAttribute() { + Attribute("parent_uid", String, "The UID of the parent project, should be empty if there is none", func() { + // No Format(FormatUUID) is included because this attribute can be an empty string if there is no parent project. + // However, the attribute should in practice be a UUID. There is server code to validate this. + Example("7cad5a8d-19d0-41a4-81a6-043453daf9ee") + }) +} + +// ProjectAuditorsAttribute is the DSL attribute for a project auditors. +func ProjectAuditorsAttribute() { + Attribute("auditors", ArrayOf(String), "A list of project auditors by their user IDs", func() { + Example([]string{"user123", "user456"}) + }) +} + +// ProjectWritersAttribute is the DSL attribute for a project writers. +func ProjectWritersAttribute() { + Attribute("writers", ArrayOf(String), "A list of project writers by their user IDs", func() { + Example([]string{"user123", "user456"}) + }) +} + +// +// Error types +// + +// BadRequestError is the DSL type for a bad request error. +var BadRequestError = Type("BadRequestError", func() { + Attribute("code", String, "HTTP status code", func() { + Example("400") + }) + Attribute("message", String, "Error message", func() { + Example("The request was invalid.") + }) + Required("code", "message") +}) + +// NotFoundError is the DSL type for a not found error. +var NotFoundError = Type("NotFoundError", func() { + Attribute("code", String, "HTTP status code", func() { + Example("404") + }) + Attribute("message", String, "Error message", func() { + Example("The resource was not found.") + }) + Required("code", "message") +}) + +// ConflictError is the DSL type for a conflict error. +var ConflictError = Type("ConflictError", func() { + Attribute("code", String, "HTTP status code", func() { + Example("409") + }) + Attribute("message", String, "Error message", func() { + Example("The resource already exists.") + }) + Required("code", "message") +}) + +// InternalServerError is the DSL type for an internal server error. +var InternalServerError = Type("InternalServerError", func() { + Attribute("code", String, "HTTP status code", func() { + Example("500") + }) + Attribute("message", String, "Error message", func() { + Example("An internal server error occurred.") + }) + Required("code", "message") +}) + +// ServiceUnavailableError is the DSL type for a service unavailable error. +var ServiceUnavailableError = Type("ServiceUnavailableError", func() { + Attribute("code", String, "HTTP status code", func() { + Example("503") + }) + Attribute("message", String, "Error message", func() { + Example("The service is unavailable.") + }) + Required("code", "message") +}) diff --git a/cmd/project-api/gen/http/cli/project_service/cli.go b/cmd/project-api/gen/http/cli/project_service/cli.go new file mode 100644 index 0000000..c662b0e --- /dev/null +++ b/cmd/project-api/gen/http/cli/project_service/cli.go @@ -0,0 +1,330 @@ +// Code generated by goa v3.21.1, DO NOT EDIT. +// +// project-service HTTP client CLI support package +// +// Command: +// $ goa gen +// github.com/linuxfoundation/lfx-v2-project-service/cmd/project-api/design + +package cli + +import ( + "flag" + "fmt" + "net/http" + "os" + + projectservicec "github.com/linuxfoundation/lfx-v2-project-service/cmd/project-api/gen/http/project_service/client" + goahttp "goa.design/goa/v3/http" + goa "goa.design/goa/v3/pkg" +) + +// UsageCommands returns the set of commands and sub-commands using the format +// +// command (subcommand1|subcommand2|...) +func UsageCommands() string { + return `project-service (get-projects|create-project|get-one-project|update-project|delete-project|readyz|livez) +` +} + +// UsageExamples produces an example of a valid invocation of the CLI tool. +func UsageExamples() string { + return os.Args[0] + ` project-service get-projects --version "1" --bearer-token "eyJhbGci..."` + "\n" + + "" +} + +// ParseEndpoint returns the endpoint and payload as specified on the command +// line. +func ParseEndpoint( + scheme, host string, + doer goahttp.Doer, + enc func(*http.Request) goahttp.Encoder, + dec func(*http.Response) goahttp.Decoder, + restore bool, +) (goa.Endpoint, any, error) { + var ( + projectServiceFlags = flag.NewFlagSet("project-service", flag.ContinueOnError) + + projectServiceGetProjectsFlags = flag.NewFlagSet("get-projects", flag.ExitOnError) + projectServiceGetProjectsVersionFlag = projectServiceGetProjectsFlags.String("version", "", "") + projectServiceGetProjectsBearerTokenFlag = projectServiceGetProjectsFlags.String("bearer-token", "", "") + + projectServiceCreateProjectFlags = flag.NewFlagSet("create-project", flag.ExitOnError) + projectServiceCreateProjectBodyFlag = projectServiceCreateProjectFlags.String("body", "REQUIRED", "") + projectServiceCreateProjectVersionFlag = projectServiceCreateProjectFlags.String("version", "", "") + projectServiceCreateProjectBearerTokenFlag = projectServiceCreateProjectFlags.String("bearer-token", "", "") + + projectServiceGetOneProjectFlags = flag.NewFlagSet("get-one-project", flag.ExitOnError) + projectServiceGetOneProjectIDFlag = projectServiceGetOneProjectFlags.String("id", "REQUIRED", "Project ID -- v2 id, not related to v1 id directly") + projectServiceGetOneProjectVersionFlag = projectServiceGetOneProjectFlags.String("version", "", "") + projectServiceGetOneProjectBearerTokenFlag = projectServiceGetOneProjectFlags.String("bearer-token", "", "") + + projectServiceUpdateProjectFlags = flag.NewFlagSet("update-project", flag.ExitOnError) + projectServiceUpdateProjectBodyFlag = projectServiceUpdateProjectFlags.String("body", "REQUIRED", "") + projectServiceUpdateProjectIDFlag = projectServiceUpdateProjectFlags.String("id", "REQUIRED", "Project ID -- v2 id, not related to v1 id directly") + projectServiceUpdateProjectVersionFlag = projectServiceUpdateProjectFlags.String("version", "", "") + projectServiceUpdateProjectBearerTokenFlag = projectServiceUpdateProjectFlags.String("bearer-token", "", "") + projectServiceUpdateProjectEtagFlag = projectServiceUpdateProjectFlags.String("etag", "", "") + + projectServiceDeleteProjectFlags = flag.NewFlagSet("delete-project", flag.ExitOnError) + projectServiceDeleteProjectIDFlag = projectServiceDeleteProjectFlags.String("id", "REQUIRED", "Project ID -- v2 id, not related to v1 id directly") + projectServiceDeleteProjectVersionFlag = projectServiceDeleteProjectFlags.String("version", "", "") + projectServiceDeleteProjectBearerTokenFlag = projectServiceDeleteProjectFlags.String("bearer-token", "", "") + projectServiceDeleteProjectEtagFlag = projectServiceDeleteProjectFlags.String("etag", "", "") + + projectServiceReadyzFlags = flag.NewFlagSet("readyz", flag.ExitOnError) + + projectServiceLivezFlags = flag.NewFlagSet("livez", flag.ExitOnError) + ) + projectServiceFlags.Usage = projectServiceUsage + projectServiceGetProjectsFlags.Usage = projectServiceGetProjectsUsage + projectServiceCreateProjectFlags.Usage = projectServiceCreateProjectUsage + projectServiceGetOneProjectFlags.Usage = projectServiceGetOneProjectUsage + projectServiceUpdateProjectFlags.Usage = projectServiceUpdateProjectUsage + projectServiceDeleteProjectFlags.Usage = projectServiceDeleteProjectUsage + projectServiceReadyzFlags.Usage = projectServiceReadyzUsage + projectServiceLivezFlags.Usage = projectServiceLivezUsage + + if err := flag.CommandLine.Parse(os.Args[1:]); err != nil { + return nil, nil, err + } + + if flag.NArg() < 2 { // two non flag args are required: SERVICE and ENDPOINT (aka COMMAND) + return nil, nil, fmt.Errorf("not enough arguments") + } + + var ( + svcn string + svcf *flag.FlagSet + ) + { + svcn = flag.Arg(0) + switch svcn { + case "project-service": + svcf = projectServiceFlags + default: + return nil, nil, fmt.Errorf("unknown service %q", svcn) + } + } + if err := svcf.Parse(flag.Args()[1:]); err != nil { + return nil, nil, err + } + + var ( + epn string + epf *flag.FlagSet + ) + { + epn = svcf.Arg(0) + switch svcn { + case "project-service": + switch epn { + case "get-projects": + epf = projectServiceGetProjectsFlags + + case "create-project": + epf = projectServiceCreateProjectFlags + + case "get-one-project": + epf = projectServiceGetOneProjectFlags + + case "update-project": + epf = projectServiceUpdateProjectFlags + + case "delete-project": + epf = projectServiceDeleteProjectFlags + + case "readyz": + epf = projectServiceReadyzFlags + + case "livez": + epf = projectServiceLivezFlags + + } + + } + } + if epf == nil { + return nil, nil, fmt.Errorf("unknown %q endpoint %q", svcn, epn) + } + + // Parse endpoint flags if any + if svcf.NArg() > 1 { + if err := epf.Parse(svcf.Args()[1:]); err != nil { + return nil, nil, err + } + } + + var ( + data any + endpoint goa.Endpoint + err error + ) + { + switch svcn { + case "project-service": + c := projectservicec.NewClient(scheme, host, doer, enc, dec, restore) + switch epn { + case "get-projects": + endpoint = c.GetProjects() + data, err = projectservicec.BuildGetProjectsPayload(*projectServiceGetProjectsVersionFlag, *projectServiceGetProjectsBearerTokenFlag) + case "create-project": + endpoint = c.CreateProject() + data, err = projectservicec.BuildCreateProjectPayload(*projectServiceCreateProjectBodyFlag, *projectServiceCreateProjectVersionFlag, *projectServiceCreateProjectBearerTokenFlag) + case "get-one-project": + endpoint = c.GetOneProject() + data, err = projectservicec.BuildGetOneProjectPayload(*projectServiceGetOneProjectIDFlag, *projectServiceGetOneProjectVersionFlag, *projectServiceGetOneProjectBearerTokenFlag) + case "update-project": + endpoint = c.UpdateProject() + data, err = projectservicec.BuildUpdateProjectPayload(*projectServiceUpdateProjectBodyFlag, *projectServiceUpdateProjectIDFlag, *projectServiceUpdateProjectVersionFlag, *projectServiceUpdateProjectBearerTokenFlag, *projectServiceUpdateProjectEtagFlag) + case "delete-project": + endpoint = c.DeleteProject() + data, err = projectservicec.BuildDeleteProjectPayload(*projectServiceDeleteProjectIDFlag, *projectServiceDeleteProjectVersionFlag, *projectServiceDeleteProjectBearerTokenFlag, *projectServiceDeleteProjectEtagFlag) + case "readyz": + endpoint = c.Readyz() + case "livez": + endpoint = c.Livez() + } + } + } + if err != nil { + return nil, nil, err + } + + return endpoint, data, nil +} + +// projectServiceUsage displays the usage of the project-service command and +// its subcommands. +func projectServiceUsage() { + fmt.Fprintf(os.Stderr, `The project service provides LFX Project resources. +Usage: + %[1]s [globalflags] project-service COMMAND [flags] + +COMMAND: + get-projects: Get all projects. + create-project: Create a new project. + get-one-project: Get a single project. + update-project: Update an existing project. + delete-project: Delete an existing project. + readyz: Check if the service is able to take inbound requests. + livez: Check if the service is alive. + +Additional help: + %[1]s project-service COMMAND --help +`, os.Args[0]) +} +func projectServiceGetProjectsUsage() { + fmt.Fprintf(os.Stderr, `%[1]s [flags] project-service get-projects -version STRING -bearer-token STRING + +Get all projects. + -version STRING: + -bearer-token STRING: + +Example: + %[1]s project-service get-projects --version "1" --bearer-token "eyJhbGci..." +`, os.Args[0]) +} + +func projectServiceCreateProjectUsage() { + fmt.Fprintf(os.Stderr, `%[1]s [flags] project-service create-project -body JSON -version STRING -bearer-token STRING + +Create a new project. + -body JSON: + -version STRING: + -bearer-token STRING: + +Example: + %[1]s project-service create-project --body '{ + "auditors": [ + "user123", + "user456" + ], + "description": "project foo is a project about bar", + "name": "Foo Foundation", + "parent_uid": "7cad5a8d-19d0-41a4-81a6-043453daf9ee", + "public": true, + "slug": "project-slug", + "writers": [ + "user123", + "user456" + ] + }' --version "1" --bearer-token "eyJhbGci..." +`, os.Args[0]) +} + +func projectServiceGetOneProjectUsage() { + fmt.Fprintf(os.Stderr, `%[1]s [flags] project-service get-one-project -id STRING -version STRING -bearer-token STRING + +Get a single project. + -id STRING: Project ID -- v2 id, not related to v1 id directly + -version STRING: + -bearer-token STRING: + +Example: + %[1]s project-service get-one-project --id "7cad5a8d-19d0-41a4-81a6-043453daf9ee" --version "1" --bearer-token "eyJhbGci..." +`, os.Args[0]) +} + +func projectServiceUpdateProjectUsage() { + fmt.Fprintf(os.Stderr, `%[1]s [flags] project-service update-project -body JSON -id STRING -version STRING -bearer-token STRING -etag STRING + +Update an existing project. + -body JSON: + -id STRING: Project ID -- v2 id, not related to v1 id directly + -version STRING: + -bearer-token STRING: + -etag STRING: + +Example: + %[1]s project-service update-project --body '{ + "auditors": [ + "user123", + "user456" + ], + "description": "project foo is a project about bar", + "name": "Foo Foundation", + "parent_uid": "7cad5a8d-19d0-41a4-81a6-043453daf9ee", + "public": true, + "slug": "project-slug", + "writers": [ + "user123", + "user456" + ] + }' --id "7cad5a8d-19d0-41a4-81a6-043453daf9ee" --version "1" --bearer-token "eyJhbGci..." --etag "123" +`, os.Args[0]) +} + +func projectServiceDeleteProjectUsage() { + fmt.Fprintf(os.Stderr, `%[1]s [flags] project-service delete-project -id STRING -version STRING -bearer-token STRING -etag STRING + +Delete an existing project. + -id STRING: Project ID -- v2 id, not related to v1 id directly + -version STRING: + -bearer-token STRING: + -etag STRING: + +Example: + %[1]s project-service delete-project --id "7cad5a8d-19d0-41a4-81a6-043453daf9ee" --version "1" --bearer-token "eyJhbGci..." --etag "123" +`, os.Args[0]) +} + +func projectServiceReadyzUsage() { + fmt.Fprintf(os.Stderr, `%[1]s [flags] project-service readyz + +Check if the service is able to take inbound requests. + +Example: + %[1]s project-service readyz +`, os.Args[0]) +} + +func projectServiceLivezUsage() { + fmt.Fprintf(os.Stderr, `%[1]s [flags] project-service livez + +Check if the service is alive. + +Example: + %[1]s project-service livez +`, os.Args[0]) +} diff --git a/cmd/project-api/gen/http/openapi.json b/cmd/project-api/gen/http/openapi.json new file mode 100644 index 0000000..b6bb77b --- /dev/null +++ b/cmd/project-api/gen/http/openapi.json @@ -0,0 +1 @@ +{"swagger":"2.0","info":{"title":"","version":"0.0.1"},"host":"localhost:80","consumes":["application/json","application/xml","application/gob"],"produces":["application/json","application/xml","application/gob"],"paths":{"/livez":{"get":{"tags":["project-service"],"summary":"livez project-service","description":"Check if the service is alive.","operationId":"project-service#livez","produces":["text/plain"],"responses":{"200":{"description":"OK response.","schema":{"type":"string","format":"byte"}}},"schemes":["http"]}},"/openapi.json":{"get":{"tags":["project-service"],"summary":"Download gen/http/openapi3.json","operationId":"project-service#/openapi.json","responses":{"200":{"description":"File downloaded","schema":{"type":"file"}}},"schemes":["http"]}},"/projects":{"get":{"tags":["project-service"],"summary":"get-projects project-service","description":"Get all projects.","operationId":"project-service#get-projects","parameters":[{"name":"v","in":"query","description":"Version of the API","required":false,"type":"string","enum":["1"]},{"name":"Authorization","in":"header","description":"JWT token issued by Heimdall","required":false,"type":"string"}],"responses":{"200":{"description":"OK response.","schema":{"$ref":"#/definitions/ProjectServiceGetProjectsResponseBody","required":["projects"]},"headers":{"Cache-Control":{"description":"Cache control header","type":"string"}}},"400":{"description":"Bad Request response.","schema":{"$ref":"#/definitions/BadRequestError","required":["code","message"]}},"500":{"description":"Internal Server Error response.","schema":{"$ref":"#/definitions/InternalServerError","required":["code","message"]}},"503":{"description":"Service Unavailable response.","schema":{"$ref":"#/definitions/ServiceUnavailableError","required":["code","message"]}}},"schemes":["http"],"security":[{"jwt_header_Authorization":[]}]},"post":{"tags":["project-service"],"summary":"create-project project-service","description":"Create a new project.","operationId":"project-service#create-project","parameters":[{"name":"v","in":"query","description":"Version of the API","required":false,"type":"string","enum":["1"]},{"name":"Authorization","in":"header","description":"JWT token issued by Heimdall","required":false,"type":"string"},{"name":"Create-ProjectRequestBody","in":"body","required":true,"schema":{"$ref":"#/definitions/ProjectServiceCreateProjectRequestBody","required":["slug","description","name"]}}],"responses":{"201":{"description":"Created response.","schema":{"$ref":"#/definitions/Project"}},"400":{"description":"Bad Request response.","schema":{"$ref":"#/definitions/BadRequestError","required":["code","message"]}},"409":{"description":"Conflict response.","schema":{"$ref":"#/definitions/ConflictError","required":["code","message"]}},"500":{"description":"Internal Server Error response.","schema":{"$ref":"#/definitions/InternalServerError","required":["code","message"]}},"503":{"description":"Service Unavailable response.","schema":{"$ref":"#/definitions/ServiceUnavailableError","required":["code","message"]}}},"schemes":["http"],"security":[{"jwt_header_Authorization":[]}]}},"/projects/{id}":{"get":{"tags":["project-service"],"summary":"get-one-project project-service","description":"Get a single project.","operationId":"project-service#get-one-project","parameters":[{"name":"v","in":"query","description":"Version of the API","required":false,"type":"string","enum":["1"]},{"name":"id","in":"path","description":"Project ID -- v2 id, not related to v1 id directly","required":true,"type":"string","format":"uuid"},{"name":"Authorization","in":"header","description":"JWT token issued by Heimdall","required":false,"type":"string"}],"responses":{"200":{"description":"OK response.","schema":{"$ref":"#/definitions/ProjectServiceGetOneProjectResponseBody"},"headers":{"ETag":{"description":"ETag header value","type":"string"}}},"404":{"description":"Not Found response.","schema":{"$ref":"#/definitions/NotFoundError","required":["code","message"]}},"500":{"description":"Internal Server Error response.","schema":{"$ref":"#/definitions/InternalServerError","required":["code","message"]}},"503":{"description":"Service Unavailable response.","schema":{"$ref":"#/definitions/ServiceUnavailableError","required":["code","message"]}}},"schemes":["http"],"security":[{"jwt_header_Authorization":[]}]},"put":{"tags":["project-service"],"summary":"update-project project-service","description":"Update an existing project.","operationId":"project-service#update-project","parameters":[{"name":"v","in":"query","description":"Version of the API","required":false,"type":"string","enum":["1"]},{"name":"id","in":"path","description":"Project ID -- v2 id, not related to v1 id directly","required":true,"type":"string","format":"uuid"},{"name":"Authorization","in":"header","description":"JWT token issued by Heimdall","required":false,"type":"string"},{"name":"ETag","in":"header","description":"ETag header value","required":false,"type":"string"},{"name":"Update-ProjectRequestBody","in":"body","required":true,"schema":{"$ref":"#/definitions/ProjectServiceUpdateProjectRequestBody","required":["slug","description","name"]}}],"responses":{"200":{"description":"OK response.","schema":{"$ref":"#/definitions/Project"}},"400":{"description":"Bad Request response.","schema":{"$ref":"#/definitions/BadRequestError","required":["code","message"]}},"404":{"description":"Not Found response.","schema":{"$ref":"#/definitions/NotFoundError","required":["code","message"]}},"500":{"description":"Internal Server Error response.","schema":{"$ref":"#/definitions/InternalServerError","required":["code","message"]}},"503":{"description":"Service Unavailable response.","schema":{"$ref":"#/definitions/ServiceUnavailableError","required":["code","message"]}}},"schemes":["http"],"security":[{"jwt_header_Authorization":[]}]},"delete":{"tags":["project-service"],"summary":"delete-project project-service","description":"Delete an existing project.","operationId":"project-service#delete-project","parameters":[{"name":"v","in":"query","description":"Version of the API","required":false,"type":"string","enum":["1"]},{"name":"id","in":"path","description":"Project ID -- v2 id, not related to v1 id directly","required":true,"type":"string","format":"uuid"},{"name":"Authorization","in":"header","description":"JWT token issued by Heimdall","required":false,"type":"string"},{"name":"ETag","in":"header","description":"ETag header value","required":false,"type":"string"}],"responses":{"204":{"description":"No Content response."},"400":{"description":"Bad Request response.","schema":{"$ref":"#/definitions/BadRequestError","required":["code","message"]}},"404":{"description":"Not Found response.","schema":{"$ref":"#/definitions/NotFoundError","required":["code","message"]}},"500":{"description":"Internal Server Error response.","schema":{"$ref":"#/definitions/InternalServerError","required":["code","message"]}},"503":{"description":"Service Unavailable response.","schema":{"$ref":"#/definitions/ServiceUnavailableError","required":["code","message"]}}},"schemes":["http"],"security":[{"jwt_header_Authorization":[]}]}},"/readyz":{"get":{"tags":["project-service"],"summary":"readyz project-service","description":"Check if the service is able to take inbound requests.","operationId":"project-service#readyz","produces":["text/plain"],"responses":{"200":{"description":"OK response.","schema":{"type":"string","format":"byte"}},"503":{"description":"Service Unavailable response.","schema":{"$ref":"#/definitions/ServiceUnavailableError","required":["code","message"]}}},"schemes":["http"]}}},"definitions":{"BadRequestError":{"title":"BadRequestError","type":"object","properties":{"code":{"type":"string","description":"HTTP status code","example":"400"},"message":{"type":"string","description":"Error message","example":"The request was invalid."}},"description":"Bad request","example":{"code":"400","message":"The request was invalid."},"required":["code","message"]},"ConflictError":{"title":"ConflictError","type":"object","properties":{"code":{"type":"string","description":"HTTP status code","example":"409"},"message":{"type":"string","description":"Error message","example":"The resource already exists."}},"description":"Conflict","example":{"code":"409","message":"The resource already exists."},"required":["code","message"]},"InternalServerError":{"title":"InternalServerError","type":"object","properties":{"code":{"type":"string","description":"HTTP status code","example":"500"},"message":{"type":"string","description":"Error message","example":"An internal server error occurred."}},"description":"Internal server error","example":{"code":"500","message":"An internal server error occurred."},"required":["code","message"]},"NotFoundError":{"title":"NotFoundError","type":"object","properties":{"code":{"type":"string","description":"HTTP status code","example":"404"},"message":{"type":"string","description":"Error message","example":"The resource was not found."}},"description":"Resource not found","example":{"code":"404","message":"The resource was not found."},"required":["code","message"]},"Project":{"title":"Project","type":"object","properties":{"auditors":{"type":"array","items":{"type":"string","example":"Ex architecto repellat non earum cumque illo."},"description":"A list of project auditors by their user IDs","example":["user123","user456"]},"description":{"type":"string","description":"A description of the project","example":"project foo is a project about bar"},"id":{"type":"string","description":"Project ID -- v2 id, not related to v1 id directly","example":"7cad5a8d-19d0-41a4-81a6-043453daf9ee","format":"uuid"},"name":{"type":"string","description":"The pretty name of the project","example":"Foo Foundation"},"parent_uid":{"type":"string","description":"The UID of the parent project, should be empty if there is none","example":"7cad5a8d-19d0-41a4-81a6-043453daf9ee"},"public":{"type":"boolean","description":"Whether the project is public","example":true},"slug":{"type":"string","description":"Project slug, a short slugified name of the project","example":"project-slug","format":"regexp","pattern":"^[a-z][a-z0-9_\\-]*[a-z0-9]$"},"writers":{"type":"array","items":{"type":"string","example":"Est et."},"description":"A list of project writers by their user IDs","example":["user123","user456"]}},"description":"A representation of LFX Projects.","example":{"auditors":["user123","user456"],"description":"project foo is a project about bar","id":"7cad5a8d-19d0-41a4-81a6-043453daf9ee","name":"Foo Foundation","parent_uid":"7cad5a8d-19d0-41a4-81a6-043453daf9ee","public":true,"slug":"project-slug","writers":["user123","user456"]}},"ProjectServiceCreateProjectRequestBody":{"title":"ProjectServiceCreateProjectRequestBody","type":"object","properties":{"auditors":{"type":"array","items":{"type":"string","example":"Quia aut vero consequatur qui."},"description":"A list of project auditors by their user IDs","example":["user123","user456"]},"description":{"type":"string","description":"A description of the project","example":"project foo is a project about bar"},"name":{"type":"string","description":"The pretty name of the project","example":"Foo Foundation"},"parent_uid":{"type":"string","description":"The UID of the parent project, should be empty if there is none","example":"7cad5a8d-19d0-41a4-81a6-043453daf9ee"},"public":{"type":"boolean","description":"Whether the project is public","example":true},"slug":{"type":"string","description":"Project slug, a short slugified name of the project","example":"project-slug","format":"regexp","pattern":"^[a-z][a-z0-9_\\-]*[a-z0-9]$"},"writers":{"type":"array","items":{"type":"string","example":"Temporibus in cupiditate quod consequatur quo."},"description":"A list of project writers by their user IDs","example":["user123","user456"]}},"example":{"auditors":["user123","user456"],"description":"project foo is a project about bar","name":"Foo Foundation","parent_uid":"7cad5a8d-19d0-41a4-81a6-043453daf9ee","public":true,"slug":"project-slug","writers":["user123","user456"]},"required":["slug","description","name"]},"ProjectServiceGetOneProjectResponseBody":{"title":"ProjectServiceGetOneProjectResponseBody","$ref":"#/definitions/Project"},"ProjectServiceGetProjectsResponseBody":{"title":"ProjectServiceGetProjectsResponseBody","type":"object","properties":{"projects":{"type":"array","items":{"$ref":"#/definitions/Project"},"description":"Resources found","example":[{"auditors":["user123","user456"],"description":"project foo is a project about bar","id":"7cad5a8d-19d0-41a4-81a6-043453daf9ee","name":"Foo Foundation","parent_uid":"7cad5a8d-19d0-41a4-81a6-043453daf9ee","public":true,"slug":"project-slug","writers":["user123","user456"]},{"auditors":["user123","user456"],"description":"project foo is a project about bar","id":"7cad5a8d-19d0-41a4-81a6-043453daf9ee","name":"Foo Foundation","parent_uid":"7cad5a8d-19d0-41a4-81a6-043453daf9ee","public":true,"slug":"project-slug","writers":["user123","user456"]},{"auditors":["user123","user456"],"description":"project foo is a project about bar","id":"7cad5a8d-19d0-41a4-81a6-043453daf9ee","name":"Foo Foundation","parent_uid":"7cad5a8d-19d0-41a4-81a6-043453daf9ee","public":true,"slug":"project-slug","writers":["user123","user456"]},{"auditors":["user123","user456"],"description":"project foo is a project about bar","id":"7cad5a8d-19d0-41a4-81a6-043453daf9ee","name":"Foo Foundation","parent_uid":"7cad5a8d-19d0-41a4-81a6-043453daf9ee","public":true,"slug":"project-slug","writers":["user123","user456"]}]}},"example":{"projects":[{"auditors":["user123","user456"],"description":"project foo is a project about bar","id":"7cad5a8d-19d0-41a4-81a6-043453daf9ee","name":"Foo Foundation","parent_uid":"7cad5a8d-19d0-41a4-81a6-043453daf9ee","public":true,"slug":"project-slug","writers":["user123","user456"]},{"auditors":["user123","user456"],"description":"project foo is a project about bar","id":"7cad5a8d-19d0-41a4-81a6-043453daf9ee","name":"Foo Foundation","parent_uid":"7cad5a8d-19d0-41a4-81a6-043453daf9ee","public":true,"slug":"project-slug","writers":["user123","user456"]}]},"required":["projects"]},"ProjectServiceUpdateProjectRequestBody":{"title":"ProjectServiceUpdateProjectRequestBody","type":"object","properties":{"auditors":{"type":"array","items":{"type":"string","example":"Voluptatibus dolores et."},"description":"A list of project auditors by their user IDs","example":["user123","user456"]},"description":{"type":"string","description":"A description of the project","example":"project foo is a project about bar"},"name":{"type":"string","description":"The pretty name of the project","example":"Foo Foundation"},"parent_uid":{"type":"string","description":"The UID of the parent project, should be empty if there is none","example":"7cad5a8d-19d0-41a4-81a6-043453daf9ee"},"public":{"type":"boolean","description":"Whether the project is public","example":true},"slug":{"type":"string","description":"Project slug, a short slugified name of the project","example":"project-slug","format":"regexp","pattern":"^[a-z][a-z0-9_\\-]*[a-z0-9]$"},"writers":{"type":"array","items":{"type":"string","example":"Eum ipsam."},"description":"A list of project writers by their user IDs","example":["user123","user456"]}},"example":{"auditors":["user123","user456"],"description":"project foo is a project about bar","name":"Foo Foundation","parent_uid":"7cad5a8d-19d0-41a4-81a6-043453daf9ee","public":true,"slug":"project-slug","writers":["user123","user456"]},"required":["slug","description","name"]},"ServiceUnavailableError":{"title":"ServiceUnavailableError","type":"object","properties":{"code":{"type":"string","description":"HTTP status code","example":"503"},"message":{"type":"string","description":"Error message","example":"The service is unavailable."}},"description":"Service unavailable","example":{"code":"503","message":"The service is unavailable."},"required":["code","message"]}},"securityDefinitions":{"jwt_header_Authorization":{"type":"apiKey","description":"Heimdall authorization","name":"Authorization","in":"header"}}} \ No newline at end of file diff --git a/cmd/project-api/gen/http/openapi.yaml b/cmd/project-api/gen/http/openapi.yaml new file mode 100644 index 0000000..d3ecb65 --- /dev/null +++ b/cmd/project-api/gen/http/openapi.yaml @@ -0,0 +1,764 @@ +swagger: "2.0" +info: + title: "" + version: 0.0.1 +host: localhost:80 +consumes: + - application/json + - application/xml + - application/gob +produces: + - application/json + - application/xml + - application/gob +paths: + /livez: + get: + tags: + - project-service + summary: livez project-service + description: Check if the service is alive. + operationId: project-service#livez + produces: + - text/plain + responses: + "200": + description: OK response. + schema: + type: string + format: byte + schemes: + - http + /openapi.json: + get: + tags: + - project-service + summary: Download gen/http/openapi3.json + operationId: project-service#/openapi.json + responses: + "200": + description: File downloaded + schema: + type: file + schemes: + - http + /projects: + get: + tags: + - project-service + summary: get-projects project-service + description: Get all projects. + operationId: project-service#get-projects + parameters: + - name: v + in: query + description: Version of the API + required: false + type: string + enum: + - "1" + - name: Authorization + in: header + description: JWT token issued by Heimdall + required: false + type: string + responses: + "200": + description: OK response. + schema: + $ref: '#/definitions/ProjectServiceGetProjectsResponseBody' + required: + - projects + headers: + Cache-Control: + description: Cache control header + type: string + "400": + description: Bad Request response. + schema: + $ref: '#/definitions/BadRequestError' + required: + - code + - message + "500": + description: Internal Server Error response. + schema: + $ref: '#/definitions/InternalServerError' + required: + - code + - message + "503": + description: Service Unavailable response. + schema: + $ref: '#/definitions/ServiceUnavailableError' + required: + - code + - message + schemes: + - http + security: + - jwt_header_Authorization: [] + post: + tags: + - project-service + summary: create-project project-service + description: Create a new project. + operationId: project-service#create-project + parameters: + - name: v + in: query + description: Version of the API + required: false + type: string + enum: + - "1" + - name: Authorization + in: header + description: JWT token issued by Heimdall + required: false + type: string + - name: Create-ProjectRequestBody + in: body + required: true + schema: + $ref: '#/definitions/ProjectServiceCreateProjectRequestBody' + required: + - slug + - description + - name + responses: + "201": + description: Created response. + schema: + $ref: '#/definitions/Project' + "400": + description: Bad Request response. + schema: + $ref: '#/definitions/BadRequestError' + required: + - code + - message + "409": + description: Conflict response. + schema: + $ref: '#/definitions/ConflictError' + required: + - code + - message + "500": + description: Internal Server Error response. + schema: + $ref: '#/definitions/InternalServerError' + required: + - code + - message + "503": + description: Service Unavailable response. + schema: + $ref: '#/definitions/ServiceUnavailableError' + required: + - code + - message + schemes: + - http + security: + - jwt_header_Authorization: [] + /projects/{id}: + get: + tags: + - project-service + summary: get-one-project project-service + description: Get a single project. + operationId: project-service#get-one-project + parameters: + - name: v + in: query + description: Version of the API + required: false + type: string + enum: + - "1" + - name: id + in: path + description: Project ID -- v2 id, not related to v1 id directly + required: true + type: string + format: uuid + - name: Authorization + in: header + description: JWT token issued by Heimdall + required: false + type: string + responses: + "200": + description: OK response. + schema: + $ref: '#/definitions/ProjectServiceGetOneProjectResponseBody' + headers: + ETag: + description: ETag header value + type: string + "404": + description: Not Found response. + schema: + $ref: '#/definitions/NotFoundError' + required: + - code + - message + "500": + description: Internal Server Error response. + schema: + $ref: '#/definitions/InternalServerError' + required: + - code + - message + "503": + description: Service Unavailable response. + schema: + $ref: '#/definitions/ServiceUnavailableError' + required: + - code + - message + schemes: + - http + security: + - jwt_header_Authorization: [] + put: + tags: + - project-service + summary: update-project project-service + description: Update an existing project. + operationId: project-service#update-project + parameters: + - name: v + in: query + description: Version of the API + required: false + type: string + enum: + - "1" + - name: id + in: path + description: Project ID -- v2 id, not related to v1 id directly + required: true + type: string + format: uuid + - name: Authorization + in: header + description: JWT token issued by Heimdall + required: false + type: string + - name: ETag + in: header + description: ETag header value + required: false + type: string + - name: Update-ProjectRequestBody + in: body + required: true + schema: + $ref: '#/definitions/ProjectServiceUpdateProjectRequestBody' + required: + - slug + - description + - name + responses: + "200": + description: OK response. + schema: + $ref: '#/definitions/Project' + "400": + description: Bad Request response. + schema: + $ref: '#/definitions/BadRequestError' + required: + - code + - message + "404": + description: Not Found response. + schema: + $ref: '#/definitions/NotFoundError' + required: + - code + - message + "500": + description: Internal Server Error response. + schema: + $ref: '#/definitions/InternalServerError' + required: + - code + - message + "503": + description: Service Unavailable response. + schema: + $ref: '#/definitions/ServiceUnavailableError' + required: + - code + - message + schemes: + - http + security: + - jwt_header_Authorization: [] + delete: + tags: + - project-service + summary: delete-project project-service + description: Delete an existing project. + operationId: project-service#delete-project + parameters: + - name: v + in: query + description: Version of the API + required: false + type: string + enum: + - "1" + - name: id + in: path + description: Project ID -- v2 id, not related to v1 id directly + required: true + type: string + format: uuid + - name: Authorization + in: header + description: JWT token issued by Heimdall + required: false + type: string + - name: ETag + in: header + description: ETag header value + required: false + type: string + responses: + "204": + description: No Content response. + "400": + description: Bad Request response. + schema: + $ref: '#/definitions/BadRequestError' + required: + - code + - message + "404": + description: Not Found response. + schema: + $ref: '#/definitions/NotFoundError' + required: + - code + - message + "500": + description: Internal Server Error response. + schema: + $ref: '#/definitions/InternalServerError' + required: + - code + - message + "503": + description: Service Unavailable response. + schema: + $ref: '#/definitions/ServiceUnavailableError' + required: + - code + - message + schemes: + - http + security: + - jwt_header_Authorization: [] + /readyz: + get: + tags: + - project-service + summary: readyz project-service + description: Check if the service is able to take inbound requests. + operationId: project-service#readyz + produces: + - text/plain + responses: + "200": + description: OK response. + schema: + type: string + format: byte + "503": + description: Service Unavailable response. + schema: + $ref: '#/definitions/ServiceUnavailableError' + required: + - code + - message + schemes: + - http +definitions: + BadRequestError: + title: BadRequestError + type: object + properties: + code: + type: string + description: HTTP status code + example: "400" + message: + type: string + description: Error message + example: The request was invalid. + description: Bad request + example: + code: "400" + message: The request was invalid. + required: + - code + - message + ConflictError: + title: ConflictError + type: object + properties: + code: + type: string + description: HTTP status code + example: "409" + message: + type: string + description: Error message + example: The resource already exists. + description: Conflict + example: + code: "409" + message: The resource already exists. + required: + - code + - message + InternalServerError: + title: InternalServerError + type: object + properties: + code: + type: string + description: HTTP status code + example: "500" + message: + type: string + description: Error message + example: An internal server error occurred. + description: Internal server error + example: + code: "500" + message: An internal server error occurred. + required: + - code + - message + NotFoundError: + title: NotFoundError + type: object + properties: + code: + type: string + description: HTTP status code + example: "404" + message: + type: string + description: Error message + example: The resource was not found. + description: Resource not found + example: + code: "404" + message: The resource was not found. + required: + - code + - message + Project: + title: Project + type: object + properties: + auditors: + type: array + items: + type: string + example: Ex architecto repellat non earum cumque illo. + description: A list of project auditors by their user IDs + example: + - user123 + - user456 + description: + type: string + description: A description of the project + example: project foo is a project about bar + id: + type: string + description: Project ID -- v2 id, not related to v1 id directly + example: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + format: uuid + name: + type: string + description: The pretty name of the project + example: Foo Foundation + parent_uid: + type: string + description: The UID of the parent project, should be empty if there is none + example: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + public: + type: boolean + description: Whether the project is public + example: true + slug: + type: string + description: Project slug, a short slugified name of the project + example: project-slug + format: regexp + pattern: ^[a-z][a-z0-9_\-]*[a-z0-9]$ + writers: + type: array + items: + type: string + example: Est et. + description: A list of project writers by their user IDs + example: + - user123 + - user456 + description: A representation of LFX Projects. + example: + auditors: + - user123 + - user456 + description: project foo is a project about bar + id: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + name: Foo Foundation + parent_uid: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + public: true + slug: project-slug + writers: + - user123 + - user456 + ProjectServiceCreateProjectRequestBody: + title: ProjectServiceCreateProjectRequestBody + type: object + properties: + auditors: + type: array + items: + type: string + example: Quia aut vero consequatur qui. + description: A list of project auditors by their user IDs + example: + - user123 + - user456 + description: + type: string + description: A description of the project + example: project foo is a project about bar + name: + type: string + description: The pretty name of the project + example: Foo Foundation + parent_uid: + type: string + description: The UID of the parent project, should be empty if there is none + example: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + public: + type: boolean + description: Whether the project is public + example: true + slug: + type: string + description: Project slug, a short slugified name of the project + example: project-slug + format: regexp + pattern: ^[a-z][a-z0-9_\-]*[a-z0-9]$ + writers: + type: array + items: + type: string + example: Temporibus in cupiditate quod consequatur quo. + description: A list of project writers by their user IDs + example: + - user123 + - user456 + example: + auditors: + - user123 + - user456 + description: project foo is a project about bar + name: Foo Foundation + parent_uid: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + public: true + slug: project-slug + writers: + - user123 + - user456 + required: + - slug + - description + - name + ProjectServiceGetOneProjectResponseBody: + title: ProjectServiceGetOneProjectResponseBody + $ref: '#/definitions/Project' + ProjectServiceGetProjectsResponseBody: + title: ProjectServiceGetProjectsResponseBody + type: object + properties: + projects: + type: array + items: + $ref: '#/definitions/Project' + description: Resources found + example: + - auditors: + - user123 + - user456 + description: project foo is a project about bar + id: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + name: Foo Foundation + parent_uid: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + public: true + slug: project-slug + writers: + - user123 + - user456 + - auditors: + - user123 + - user456 + description: project foo is a project about bar + id: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + name: Foo Foundation + parent_uid: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + public: true + slug: project-slug + writers: + - user123 + - user456 + - auditors: + - user123 + - user456 + description: project foo is a project about bar + id: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + name: Foo Foundation + parent_uid: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + public: true + slug: project-slug + writers: + - user123 + - user456 + - auditors: + - user123 + - user456 + description: project foo is a project about bar + id: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + name: Foo Foundation + parent_uid: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + public: true + slug: project-slug + writers: + - user123 + - user456 + example: + projects: + - auditors: + - user123 + - user456 + description: project foo is a project about bar + id: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + name: Foo Foundation + parent_uid: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + public: true + slug: project-slug + writers: + - user123 + - user456 + - auditors: + - user123 + - user456 + description: project foo is a project about bar + id: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + name: Foo Foundation + parent_uid: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + public: true + slug: project-slug + writers: + - user123 + - user456 + required: + - projects + ProjectServiceUpdateProjectRequestBody: + title: ProjectServiceUpdateProjectRequestBody + type: object + properties: + auditors: + type: array + items: + type: string + example: Voluptatibus dolores et. + description: A list of project auditors by their user IDs + example: + - user123 + - user456 + description: + type: string + description: A description of the project + example: project foo is a project about bar + name: + type: string + description: The pretty name of the project + example: Foo Foundation + parent_uid: + type: string + description: The UID of the parent project, should be empty if there is none + example: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + public: + type: boolean + description: Whether the project is public + example: true + slug: + type: string + description: Project slug, a short slugified name of the project + example: project-slug + format: regexp + pattern: ^[a-z][a-z0-9_\-]*[a-z0-9]$ + writers: + type: array + items: + type: string + example: Eum ipsam. + description: A list of project writers by their user IDs + example: + - user123 + - user456 + example: + auditors: + - user123 + - user456 + description: project foo is a project about bar + name: Foo Foundation + parent_uid: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + public: true + slug: project-slug + writers: + - user123 + - user456 + required: + - slug + - description + - name + ServiceUnavailableError: + title: ServiceUnavailableError + type: object + properties: + code: + type: string + description: HTTP status code + example: "503" + message: + type: string + description: Error message + example: The service is unavailable. + description: Service unavailable + example: + code: "503" + message: The service is unavailable. + required: + - code + - message +securityDefinitions: + jwt_header_Authorization: + type: apiKey + description: Heimdall authorization + name: Authorization + in: header diff --git a/cmd/project-api/gen/http/openapi3.json b/cmd/project-api/gen/http/openapi3.json new file mode 100644 index 0000000..8185501 --- /dev/null +++ b/cmd/project-api/gen/http/openapi3.json @@ -0,0 +1 @@ +{"openapi":"3.0.3","info":{"title":"Goa API","version":"0.0.1"},"servers":[{"url":"http://localhost:80","description":"Default server for project-service"}],"paths":{"/livez":{"get":{"tags":["project-service"],"summary":"livez project-service","description":"Check if the service is alive.","operationId":"project-service#livez","responses":{"200":{"description":"OK response.","content":{"text/plain":{"schema":{"type":"string","example":"OK","format":"binary"},"example":"OK"}}}}}},"/openapi.json":{"get":{"tags":["project-service"],"summary":"Download gen/http/openapi3.json","operationId":"project-service#/openapi.json","responses":{"200":{"description":"File downloaded"}}}},"/projects":{"get":{"tags":["project-service"],"summary":"get-projects project-service","description":"Get all projects.","operationId":"project-service#get-projects","parameters":[{"name":"v","in":"query","description":"Version of the API","allowEmptyValue":true,"schema":{"type":"string","description":"Version of the API","example":"1","enum":["1"]},"example":"1"}],"responses":{"200":{"description":"OK response.","headers":{"Cache-Control":{"description":"Cache control header","schema":{"type":"string","description":"Cache control header","example":"public, max-age=300"},"example":"public, max-age=300"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetProjectsResponseBody"},"example":{"projects":[{"auditors":["user123","user456"],"description":"project foo is a project about bar","id":"7cad5a8d-19d0-41a4-81a6-043453daf9ee","name":"Foo Foundation","parent_uid":"7cad5a8d-19d0-41a4-81a6-043453daf9ee","public":true,"slug":"project-slug","writers":["user123","user456"]},{"auditors":["user123","user456"],"description":"project foo is a project about bar","id":"7cad5a8d-19d0-41a4-81a6-043453daf9ee","name":"Foo Foundation","parent_uid":"7cad5a8d-19d0-41a4-81a6-043453daf9ee","public":true,"slug":"project-slug","writers":["user123","user456"]},{"auditors":["user123","user456"],"description":"project foo is a project about bar","id":"7cad5a8d-19d0-41a4-81a6-043453daf9ee","name":"Foo Foundation","parent_uid":"7cad5a8d-19d0-41a4-81a6-043453daf9ee","public":true,"slug":"project-slug","writers":["user123","user456"]}]}}}},"400":{"description":"BadRequest: Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"},"example":{"code":"400","message":"The request was invalid."}}}},"500":{"description":"InternalServerError: Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InternalServerError"},"example":{"code":"500","message":"An internal server error occurred."}}}},"503":{"description":"ServiceUnavailable: Service unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServiceUnavailableError"},"example":{"code":"503","message":"The service is unavailable."}}}}},"security":[{"jwt_header_Authorization":[]}]},"post":{"tags":["project-service"],"summary":"create-project project-service","description":"Create a new project.","operationId":"project-service#create-project","parameters":[{"name":"v","in":"query","description":"Version of the API","allowEmptyValue":true,"schema":{"type":"string","description":"Version of the API","example":"1","enum":["1"]},"example":"1"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProjectRequestBody"},"example":{"auditors":["user123","user456"],"description":"project foo is a project about bar","name":"Foo Foundation","parent_uid":"7cad5a8d-19d0-41a4-81a6-043453daf9ee","public":true,"slug":"project-slug","writers":["user123","user456"]}}}},"responses":{"201":{"description":"Created response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Project"},"example":{"auditors":["user123","user456"],"description":"project foo is a project about bar","id":"7cad5a8d-19d0-41a4-81a6-043453daf9ee","name":"Foo Foundation","parent_uid":"7cad5a8d-19d0-41a4-81a6-043453daf9ee","public":true,"slug":"project-slug","writers":["user123","user456"]}}}},"400":{"description":"BadRequest: Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"},"example":{"code":"400","message":"The request was invalid."}}}},"409":{"description":"Conflict: Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConflictError"},"example":{"code":"409","message":"The resource already exists."}}}},"500":{"description":"InternalServerError: Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InternalServerError"},"example":{"code":"500","message":"An internal server error occurred."}}}},"503":{"description":"ServiceUnavailable: Service unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServiceUnavailableError"},"example":{"code":"503","message":"The service is unavailable."}}}}},"security":[{"jwt_header_Authorization":[]}]}},"/projects/{id}":{"delete":{"tags":["project-service"],"summary":"delete-project project-service","description":"Delete an existing project.","operationId":"project-service#delete-project","parameters":[{"name":"v","in":"query","description":"Version of the API","allowEmptyValue":true,"schema":{"type":"string","description":"Version of the API","example":"1","enum":["1"]},"example":"1"},{"name":"id","in":"path","description":"Project ID -- v2 id, not related to v1 id directly","required":true,"schema":{"type":"string","description":"Project ID -- v2 id, not related to v1 id directly","example":"7cad5a8d-19d0-41a4-81a6-043453daf9ee","format":"uuid"},"example":"7cad5a8d-19d0-41a4-81a6-043453daf9ee"},{"name":"ETag","in":"header","description":"ETag header value","allowEmptyValue":true,"schema":{"type":"string","description":"ETag header value","example":"123"},"example":"123"}],"responses":{"204":{"description":"No Content response."},"400":{"description":"BadRequest: Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"},"example":{"code":"400","message":"The request was invalid."}}}},"404":{"description":"NotFound: Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotFoundError"},"example":{"code":"404","message":"The resource was not found."}}}},"500":{"description":"InternalServerError: Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InternalServerError"},"example":{"code":"500","message":"An internal server error occurred."}}}},"503":{"description":"ServiceUnavailable: Service unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServiceUnavailableError"},"example":{"code":"503","message":"The service is unavailable."}}}}},"security":[{"jwt_header_Authorization":[]}]},"get":{"tags":["project-service"],"summary":"get-one-project project-service","description":"Get a single project.","operationId":"project-service#get-one-project","parameters":[{"name":"v","in":"query","description":"Version of the API","allowEmptyValue":true,"schema":{"type":"string","description":"Version of the API","example":"1","enum":["1"]},"example":"1"},{"name":"id","in":"path","description":"Project ID -- v2 id, not related to v1 id directly","required":true,"schema":{"type":"string","description":"Project ID -- v2 id, not related to v1 id directly","example":"7cad5a8d-19d0-41a4-81a6-043453daf9ee","format":"uuid"},"example":"7cad5a8d-19d0-41a4-81a6-043453daf9ee"}],"responses":{"200":{"description":"OK response.","headers":{"ETag":{"description":"ETag header value","schema":{"type":"string","description":"ETag header value","example":"Sequi laboriosam quis quas."},"example":"Rerum odio omnis ipsum."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Project"},"example":{"auditors":["user123","user456"],"description":"project foo is a project about bar","id":"7cad5a8d-19d0-41a4-81a6-043453daf9ee","name":"Foo Foundation","parent_uid":"7cad5a8d-19d0-41a4-81a6-043453daf9ee","public":true,"slug":"project-slug","writers":["user123","user456"]}}}},"404":{"description":"NotFound: Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotFoundError"},"example":{"code":"404","message":"The resource was not found."}}}},"500":{"description":"InternalServerError: Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InternalServerError"},"example":{"code":"500","message":"An internal server error occurred."}}}},"503":{"description":"ServiceUnavailable: Service unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServiceUnavailableError"},"example":{"code":"503","message":"The service is unavailable."}}}}},"security":[{"jwt_header_Authorization":[]}]},"put":{"tags":["project-service"],"summary":"update-project project-service","description":"Update an existing project.","operationId":"project-service#update-project","parameters":[{"name":"v","in":"query","description":"Version of the API","allowEmptyValue":true,"schema":{"type":"string","description":"Version of the API","example":"1","enum":["1"]},"example":"1"},{"name":"id","in":"path","description":"Project ID -- v2 id, not related to v1 id directly","required":true,"schema":{"type":"string","description":"Project ID -- v2 id, not related to v1 id directly","example":"7cad5a8d-19d0-41a4-81a6-043453daf9ee","format":"uuid"},"example":"7cad5a8d-19d0-41a4-81a6-043453daf9ee"},{"name":"ETag","in":"header","description":"ETag header value","allowEmptyValue":true,"schema":{"type":"string","description":"ETag header value","example":"123"},"example":"123"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProjectRequestBody"},"example":{"auditors":["user123","user456"],"description":"project foo is a project about bar","name":"Foo Foundation","parent_uid":"7cad5a8d-19d0-41a4-81a6-043453daf9ee","public":true,"slug":"project-slug","writers":["user123","user456"]}}}},"responses":{"200":{"description":"OK response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Project"},"example":{"auditors":["user123","user456"],"description":"project foo is a project about bar","id":"7cad5a8d-19d0-41a4-81a6-043453daf9ee","name":"Foo Foundation","parent_uid":"7cad5a8d-19d0-41a4-81a6-043453daf9ee","public":true,"slug":"project-slug","writers":["user123","user456"]}}}},"400":{"description":"BadRequest: Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"},"example":{"code":"400","message":"The request was invalid."}}}},"404":{"description":"NotFound: Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotFoundError"},"example":{"code":"404","message":"The resource was not found."}}}},"500":{"description":"InternalServerError: Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InternalServerError"},"example":{"code":"500","message":"An internal server error occurred."}}}},"503":{"description":"ServiceUnavailable: Service unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServiceUnavailableError"},"example":{"code":"503","message":"The service is unavailable."}}}}},"security":[{"jwt_header_Authorization":[]}]}},"/readyz":{"get":{"tags":["project-service"],"summary":"readyz project-service","description":"Check if the service is able to take inbound requests.","operationId":"project-service#readyz","responses":{"200":{"description":"OK response.","content":{"text/plain":{"schema":{"type":"string","example":"OK","format":"binary"},"example":"OK"}}},"503":{"description":"ServiceUnavailable: Service is unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServiceUnavailableError"},"example":{"code":"503","message":"The service is unavailable."}}}}}}}},"components":{"schemas":{"BadRequestError":{"type":"object","properties":{"code":{"type":"string","description":"HTTP status code","example":"400"},"message":{"type":"string","description":"Error message","example":"The request was invalid."}},"example":{"code":"400","message":"The request was invalid."},"required":["code","message"]},"ConflictError":{"type":"object","properties":{"code":{"type":"string","description":"HTTP status code","example":"409"},"message":{"type":"string","description":"Error message","example":"The resource already exists."}},"example":{"code":"409","message":"The resource already exists."},"required":["code","message"]},"CreateProjectRequestBody":{"type":"object","properties":{"auditors":{"type":"array","items":{"type":"string","example":"Quae ex voluptate voluptatem magnam quos eos."},"description":"A list of project auditors by their user IDs","example":["user123","user456"]},"description":{"type":"string","description":"A description of the project","example":"project foo is a project about bar"},"name":{"type":"string","description":"The pretty name of the project","example":"Foo Foundation"},"parent_uid":{"type":"string","description":"The UID of the parent project, should be empty if there is none","example":"7cad5a8d-19d0-41a4-81a6-043453daf9ee"},"public":{"type":"boolean","description":"Whether the project is public","example":true},"slug":{"type":"string","description":"Project slug, a short slugified name of the project","example":"project-slug","format":"regexp","pattern":"^[a-z][a-z0-9_\\-]*[a-z0-9]$"},"writers":{"type":"array","items":{"type":"string","example":"Voluptas et quibusdam veritatis sed quis."},"description":"A list of project writers by their user IDs","example":["user123","user456"]}},"example":{"auditors":["user123","user456"],"description":"project foo is a project about bar","name":"Foo Foundation","parent_uid":"7cad5a8d-19d0-41a4-81a6-043453daf9ee","public":true,"slug":"project-slug","writers":["user123","user456"]},"required":["slug","description","name"]},"GetProjectsResponseBody":{"type":"object","properties":{"projects":{"type":"array","items":{"$ref":"#/components/schemas/Project"},"description":"Resources found","example":[{"auditors":["user123","user456"],"description":"project foo is a project about bar","id":"7cad5a8d-19d0-41a4-81a6-043453daf9ee","name":"Foo Foundation","parent_uid":"7cad5a8d-19d0-41a4-81a6-043453daf9ee","public":true,"slug":"project-slug","writers":["user123","user456"]},{"auditors":["user123","user456"],"description":"project foo is a project about bar","id":"7cad5a8d-19d0-41a4-81a6-043453daf9ee","name":"Foo Foundation","parent_uid":"7cad5a8d-19d0-41a4-81a6-043453daf9ee","public":true,"slug":"project-slug","writers":["user123","user456"]},{"auditors":["user123","user456"],"description":"project foo is a project about bar","id":"7cad5a8d-19d0-41a4-81a6-043453daf9ee","name":"Foo Foundation","parent_uid":"7cad5a8d-19d0-41a4-81a6-043453daf9ee","public":true,"slug":"project-slug","writers":["user123","user456"]},{"auditors":["user123","user456"],"description":"project foo is a project about bar","id":"7cad5a8d-19d0-41a4-81a6-043453daf9ee","name":"Foo Foundation","parent_uid":"7cad5a8d-19d0-41a4-81a6-043453daf9ee","public":true,"slug":"project-slug","writers":["user123","user456"]}]}},"example":{"projects":[{"auditors":["user123","user456"],"description":"project foo is a project about bar","id":"7cad5a8d-19d0-41a4-81a6-043453daf9ee","name":"Foo Foundation","parent_uid":"7cad5a8d-19d0-41a4-81a6-043453daf9ee","public":true,"slug":"project-slug","writers":["user123","user456"]},{"auditors":["user123","user456"],"description":"project foo is a project about bar","id":"7cad5a8d-19d0-41a4-81a6-043453daf9ee","name":"Foo Foundation","parent_uid":"7cad5a8d-19d0-41a4-81a6-043453daf9ee","public":true,"slug":"project-slug","writers":["user123","user456"]},{"auditors":["user123","user456"],"description":"project foo is a project about bar","id":"7cad5a8d-19d0-41a4-81a6-043453daf9ee","name":"Foo Foundation","parent_uid":"7cad5a8d-19d0-41a4-81a6-043453daf9ee","public":true,"slug":"project-slug","writers":["user123","user456"]},{"auditors":["user123","user456"],"description":"project foo is a project about bar","id":"7cad5a8d-19d0-41a4-81a6-043453daf9ee","name":"Foo Foundation","parent_uid":"7cad5a8d-19d0-41a4-81a6-043453daf9ee","public":true,"slug":"project-slug","writers":["user123","user456"]}]},"required":["projects"]},"InternalServerError":{"type":"object","properties":{"code":{"type":"string","description":"HTTP status code","example":"500"},"message":{"type":"string","description":"Error message","example":"An internal server error occurred."}},"example":{"code":"500","message":"An internal server error occurred."},"required":["code","message"]},"NotFoundError":{"type":"object","properties":{"code":{"type":"string","description":"HTTP status code","example":"404"},"message":{"type":"string","description":"Error message","example":"The resource was not found."}},"example":{"code":"404","message":"The resource was not found."},"required":["code","message"]},"Project":{"type":"object","properties":{"auditors":{"type":"array","items":{"type":"string","example":"Aperiam omnis tenetur."},"description":"A list of project auditors by their user IDs","example":["user123","user456"]},"description":{"type":"string","description":"A description of the project","example":"project foo is a project about bar"},"id":{"type":"string","description":"Project ID -- v2 id, not related to v1 id directly","example":"7cad5a8d-19d0-41a4-81a6-043453daf9ee","format":"uuid"},"name":{"type":"string","description":"The pretty name of the project","example":"Foo Foundation"},"parent_uid":{"type":"string","description":"The UID of the parent project, should be empty if there is none","example":"7cad5a8d-19d0-41a4-81a6-043453daf9ee"},"public":{"type":"boolean","description":"Whether the project is public","example":true},"slug":{"type":"string","description":"Project slug, a short slugified name of the project","example":"project-slug","format":"regexp","pattern":"^[a-z][a-z0-9_\\-]*[a-z0-9]$"},"writers":{"type":"array","items":{"type":"string","example":"Doloribus ratione debitis molestiae ut dolorum id."},"description":"A list of project writers by their user IDs","example":["user123","user456"]}},"description":"A representation of LFX Projects.","example":{"auditors":["user123","user456"],"description":"project foo is a project about bar","id":"7cad5a8d-19d0-41a4-81a6-043453daf9ee","name":"Foo Foundation","parent_uid":"7cad5a8d-19d0-41a4-81a6-043453daf9ee","public":true,"slug":"project-slug","writers":["user123","user456"]}},"ServiceUnavailableError":{"type":"object","properties":{"code":{"type":"string","description":"HTTP status code","example":"503"},"message":{"type":"string","description":"Error message","example":"The service is unavailable."}},"example":{"code":"503","message":"The service is unavailable."},"required":["code","message"]}},"securitySchemes":{"jwt_header_Authorization":{"type":"http","description":"Heimdall authorization","scheme":"bearer"}}},"tags":[{"name":"project-service","description":"The project service provides LFX Project resources."}]} \ No newline at end of file diff --git a/cmd/project-api/gen/http/openapi3.yaml b/cmd/project-api/gen/http/openapi3.yaml new file mode 100644 index 0000000..2286915 --- /dev/null +++ b/cmd/project-api/gen/http/openapi3.yaml @@ -0,0 +1,857 @@ +openapi: 3.0.3 +info: + title: Goa API + version: 0.0.1 +servers: + - url: http://localhost:80 + description: Default server for project-service +paths: + /livez: + get: + tags: + - project-service + summary: livez project-service + description: Check if the service is alive. + operationId: project-service#livez + responses: + "200": + description: OK response. + content: + text/plain: + schema: + type: string + example: OK + format: binary + example: OK + /openapi.json: + get: + tags: + - project-service + summary: Download gen/http/openapi3.json + operationId: project-service#/openapi.json + responses: + "200": + description: File downloaded + /projects: + get: + tags: + - project-service + summary: get-projects project-service + description: Get all projects. + operationId: project-service#get-projects + parameters: + - name: v + in: query + description: Version of the API + allowEmptyValue: true + schema: + type: string + description: Version of the API + example: "1" + enum: + - "1" + example: "1" + responses: + "200": + description: OK response. + headers: + Cache-Control: + description: Cache control header + schema: + type: string + description: Cache control header + example: public, max-age=300 + example: public, max-age=300 + content: + application/json: + schema: + $ref: '#/components/schemas/GetProjectsResponseBody' + example: + projects: + - auditors: + - user123 + - user456 + description: project foo is a project about bar + id: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + name: Foo Foundation + parent_uid: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + public: true + slug: project-slug + writers: + - user123 + - user456 + - auditors: + - user123 + - user456 + description: project foo is a project about bar + id: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + name: Foo Foundation + parent_uid: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + public: true + slug: project-slug + writers: + - user123 + - user456 + - auditors: + - user123 + - user456 + description: project foo is a project about bar + id: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + name: Foo Foundation + parent_uid: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + public: true + slug: project-slug + writers: + - user123 + - user456 + "400": + description: 'BadRequest: Bad request' + content: + application/json: + schema: + $ref: '#/components/schemas/BadRequestError' + example: + code: "400" + message: The request was invalid. + "500": + description: 'InternalServerError: Internal server error' + content: + application/json: + schema: + $ref: '#/components/schemas/InternalServerError' + example: + code: "500" + message: An internal server error occurred. + "503": + description: 'ServiceUnavailable: Service unavailable' + content: + application/json: + schema: + $ref: '#/components/schemas/ServiceUnavailableError' + example: + code: "503" + message: The service is unavailable. + security: + - jwt_header_Authorization: [] + post: + tags: + - project-service + summary: create-project project-service + description: Create a new project. + operationId: project-service#create-project + parameters: + - name: v + in: query + description: Version of the API + allowEmptyValue: true + schema: + type: string + description: Version of the API + example: "1" + enum: + - "1" + example: "1" + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateProjectRequestBody' + example: + auditors: + - user123 + - user456 + description: project foo is a project about bar + name: Foo Foundation + parent_uid: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + public: true + slug: project-slug + writers: + - user123 + - user456 + responses: + "201": + description: Created response. + content: + application/json: + schema: + $ref: '#/components/schemas/Project' + example: + auditors: + - user123 + - user456 + description: project foo is a project about bar + id: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + name: Foo Foundation + parent_uid: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + public: true + slug: project-slug + writers: + - user123 + - user456 + "400": + description: 'BadRequest: Bad request' + content: + application/json: + schema: + $ref: '#/components/schemas/BadRequestError' + example: + code: "400" + message: The request was invalid. + "409": + description: 'Conflict: Conflict' + content: + application/json: + schema: + $ref: '#/components/schemas/ConflictError' + example: + code: "409" + message: The resource already exists. + "500": + description: 'InternalServerError: Internal server error' + content: + application/json: + schema: + $ref: '#/components/schemas/InternalServerError' + example: + code: "500" + message: An internal server error occurred. + "503": + description: 'ServiceUnavailable: Service unavailable' + content: + application/json: + schema: + $ref: '#/components/schemas/ServiceUnavailableError' + example: + code: "503" + message: The service is unavailable. + security: + - jwt_header_Authorization: [] + /projects/{id}: + delete: + tags: + - project-service + summary: delete-project project-service + description: Delete an existing project. + operationId: project-service#delete-project + parameters: + - name: v + in: query + description: Version of the API + allowEmptyValue: true + schema: + type: string + description: Version of the API + example: "1" + enum: + - "1" + example: "1" + - name: id + in: path + description: Project ID -- v2 id, not related to v1 id directly + required: true + schema: + type: string + description: Project ID -- v2 id, not related to v1 id directly + example: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + format: uuid + example: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + - name: ETag + in: header + description: ETag header value + allowEmptyValue: true + schema: + type: string + description: ETag header value + example: "123" + example: "123" + responses: + "204": + description: No Content response. + "400": + description: 'BadRequest: Bad request' + content: + application/json: + schema: + $ref: '#/components/schemas/BadRequestError' + example: + code: "400" + message: The request was invalid. + "404": + description: 'NotFound: Resource not found' + content: + application/json: + schema: + $ref: '#/components/schemas/NotFoundError' + example: + code: "404" + message: The resource was not found. + "500": + description: 'InternalServerError: Internal server error' + content: + application/json: + schema: + $ref: '#/components/schemas/InternalServerError' + example: + code: "500" + message: An internal server error occurred. + "503": + description: 'ServiceUnavailable: Service unavailable' + content: + application/json: + schema: + $ref: '#/components/schemas/ServiceUnavailableError' + example: + code: "503" + message: The service is unavailable. + security: + - jwt_header_Authorization: [] + get: + tags: + - project-service + summary: get-one-project project-service + description: Get a single project. + operationId: project-service#get-one-project + parameters: + - name: v + in: query + description: Version of the API + allowEmptyValue: true + schema: + type: string + description: Version of the API + example: "1" + enum: + - "1" + example: "1" + - name: id + in: path + description: Project ID -- v2 id, not related to v1 id directly + required: true + schema: + type: string + description: Project ID -- v2 id, not related to v1 id directly + example: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + format: uuid + example: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + responses: + "200": + description: OK response. + headers: + ETag: + description: ETag header value + schema: + type: string + description: ETag header value + example: Sequi laboriosam quis quas. + example: Rerum odio omnis ipsum. + content: + application/json: + schema: + $ref: '#/components/schemas/Project' + example: + auditors: + - user123 + - user456 + description: project foo is a project about bar + id: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + name: Foo Foundation + parent_uid: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + public: true + slug: project-slug + writers: + - user123 + - user456 + "404": + description: 'NotFound: Resource not found' + content: + application/json: + schema: + $ref: '#/components/schemas/NotFoundError' + example: + code: "404" + message: The resource was not found. + "500": + description: 'InternalServerError: Internal server error' + content: + application/json: + schema: + $ref: '#/components/schemas/InternalServerError' + example: + code: "500" + message: An internal server error occurred. + "503": + description: 'ServiceUnavailable: Service unavailable' + content: + application/json: + schema: + $ref: '#/components/schemas/ServiceUnavailableError' + example: + code: "503" + message: The service is unavailable. + security: + - jwt_header_Authorization: [] + put: + tags: + - project-service + summary: update-project project-service + description: Update an existing project. + operationId: project-service#update-project + parameters: + - name: v + in: query + description: Version of the API + allowEmptyValue: true + schema: + type: string + description: Version of the API + example: "1" + enum: + - "1" + example: "1" + - name: id + in: path + description: Project ID -- v2 id, not related to v1 id directly + required: true + schema: + type: string + description: Project ID -- v2 id, not related to v1 id directly + example: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + format: uuid + example: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + - name: ETag + in: header + description: ETag header value + allowEmptyValue: true + schema: + type: string + description: ETag header value + example: "123" + example: "123" + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateProjectRequestBody' + example: + auditors: + - user123 + - user456 + description: project foo is a project about bar + name: Foo Foundation + parent_uid: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + public: true + slug: project-slug + writers: + - user123 + - user456 + responses: + "200": + description: OK response. + content: + application/json: + schema: + $ref: '#/components/schemas/Project' + example: + auditors: + - user123 + - user456 + description: project foo is a project about bar + id: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + name: Foo Foundation + parent_uid: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + public: true + slug: project-slug + writers: + - user123 + - user456 + "400": + description: 'BadRequest: Bad request' + content: + application/json: + schema: + $ref: '#/components/schemas/BadRequestError' + example: + code: "400" + message: The request was invalid. + "404": + description: 'NotFound: Resource not found' + content: + application/json: + schema: + $ref: '#/components/schemas/NotFoundError' + example: + code: "404" + message: The resource was not found. + "500": + description: 'InternalServerError: Internal server error' + content: + application/json: + schema: + $ref: '#/components/schemas/InternalServerError' + example: + code: "500" + message: An internal server error occurred. + "503": + description: 'ServiceUnavailable: Service unavailable' + content: + application/json: + schema: + $ref: '#/components/schemas/ServiceUnavailableError' + example: + code: "503" + message: The service is unavailable. + security: + - jwt_header_Authorization: [] + /readyz: + get: + tags: + - project-service + summary: readyz project-service + description: Check if the service is able to take inbound requests. + operationId: project-service#readyz + responses: + "200": + description: OK response. + content: + text/plain: + schema: + type: string + example: OK + format: binary + example: OK + "503": + description: 'ServiceUnavailable: Service is unavailable' + content: + application/json: + schema: + $ref: '#/components/schemas/ServiceUnavailableError' + example: + code: "503" + message: The service is unavailable. +components: + schemas: + BadRequestError: + type: object + properties: + code: + type: string + description: HTTP status code + example: "400" + message: + type: string + description: Error message + example: The request was invalid. + example: + code: "400" + message: The request was invalid. + required: + - code + - message + ConflictError: + type: object + properties: + code: + type: string + description: HTTP status code + example: "409" + message: + type: string + description: Error message + example: The resource already exists. + example: + code: "409" + message: The resource already exists. + required: + - code + - message + CreateProjectRequestBody: + type: object + properties: + auditors: + type: array + items: + type: string + example: Quae ex voluptate voluptatem magnam quos eos. + description: A list of project auditors by their user IDs + example: + - user123 + - user456 + description: + type: string + description: A description of the project + example: project foo is a project about bar + name: + type: string + description: The pretty name of the project + example: Foo Foundation + parent_uid: + type: string + description: The UID of the parent project, should be empty if there is none + example: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + public: + type: boolean + description: Whether the project is public + example: true + slug: + type: string + description: Project slug, a short slugified name of the project + example: project-slug + format: regexp + pattern: ^[a-z][a-z0-9_\-]*[a-z0-9]$ + writers: + type: array + items: + type: string + example: Voluptas et quibusdam veritatis sed quis. + description: A list of project writers by their user IDs + example: + - user123 + - user456 + example: + auditors: + - user123 + - user456 + description: project foo is a project about bar + name: Foo Foundation + parent_uid: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + public: true + slug: project-slug + writers: + - user123 + - user456 + required: + - slug + - description + - name + GetProjectsResponseBody: + type: object + properties: + projects: + type: array + items: + $ref: '#/components/schemas/Project' + description: Resources found + example: + - auditors: + - user123 + - user456 + description: project foo is a project about bar + id: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + name: Foo Foundation + parent_uid: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + public: true + slug: project-slug + writers: + - user123 + - user456 + - auditors: + - user123 + - user456 + description: project foo is a project about bar + id: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + name: Foo Foundation + parent_uid: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + public: true + slug: project-slug + writers: + - user123 + - user456 + - auditors: + - user123 + - user456 + description: project foo is a project about bar + id: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + name: Foo Foundation + parent_uid: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + public: true + slug: project-slug + writers: + - user123 + - user456 + - auditors: + - user123 + - user456 + description: project foo is a project about bar + id: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + name: Foo Foundation + parent_uid: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + public: true + slug: project-slug + writers: + - user123 + - user456 + example: + projects: + - auditors: + - user123 + - user456 + description: project foo is a project about bar + id: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + name: Foo Foundation + parent_uid: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + public: true + slug: project-slug + writers: + - user123 + - user456 + - auditors: + - user123 + - user456 + description: project foo is a project about bar + id: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + name: Foo Foundation + parent_uid: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + public: true + slug: project-slug + writers: + - user123 + - user456 + - auditors: + - user123 + - user456 + description: project foo is a project about bar + id: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + name: Foo Foundation + parent_uid: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + public: true + slug: project-slug + writers: + - user123 + - user456 + - auditors: + - user123 + - user456 + description: project foo is a project about bar + id: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + name: Foo Foundation + parent_uid: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + public: true + slug: project-slug + writers: + - user123 + - user456 + required: + - projects + InternalServerError: + type: object + properties: + code: + type: string + description: HTTP status code + example: "500" + message: + type: string + description: Error message + example: An internal server error occurred. + example: + code: "500" + message: An internal server error occurred. + required: + - code + - message + NotFoundError: + type: object + properties: + code: + type: string + description: HTTP status code + example: "404" + message: + type: string + description: Error message + example: The resource was not found. + example: + code: "404" + message: The resource was not found. + required: + - code + - message + Project: + type: object + properties: + auditors: + type: array + items: + type: string + example: Aperiam omnis tenetur. + description: A list of project auditors by their user IDs + example: + - user123 + - user456 + description: + type: string + description: A description of the project + example: project foo is a project about bar + id: + type: string + description: Project ID -- v2 id, not related to v1 id directly + example: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + format: uuid + name: + type: string + description: The pretty name of the project + example: Foo Foundation + parent_uid: + type: string + description: The UID of the parent project, should be empty if there is none + example: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + public: + type: boolean + description: Whether the project is public + example: true + slug: + type: string + description: Project slug, a short slugified name of the project + example: project-slug + format: regexp + pattern: ^[a-z][a-z0-9_\-]*[a-z0-9]$ + writers: + type: array + items: + type: string + example: Doloribus ratione debitis molestiae ut dolorum id. + description: A list of project writers by their user IDs + example: + - user123 + - user456 + description: A representation of LFX Projects. + example: + auditors: + - user123 + - user456 + description: project foo is a project about bar + id: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + name: Foo Foundation + parent_uid: 7cad5a8d-19d0-41a4-81a6-043453daf9ee + public: true + slug: project-slug + writers: + - user123 + - user456 + ServiceUnavailableError: + type: object + properties: + code: + type: string + description: HTTP status code + example: "503" + message: + type: string + description: Error message + example: The service is unavailable. + example: + code: "503" + message: The service is unavailable. + required: + - code + - message + securitySchemes: + jwt_header_Authorization: + type: http + description: Heimdall authorization + scheme: bearer +tags: + - name: project-service + description: The project service provides LFX Project resources. diff --git a/cmd/project-api/gen/http/project_service/client/cli.go b/cmd/project-api/gen/http/project_service/client/cli.go new file mode 100644 index 0000000..fc96d58 --- /dev/null +++ b/cmd/project-api/gen/http/project_service/client/cli.go @@ -0,0 +1,263 @@ +// Code generated by goa v3.21.1, DO NOT EDIT. +// +// project-service HTTP client CLI support package +// +// Command: +// $ goa gen +// github.com/linuxfoundation/lfx-v2-project-service/cmd/project-api/design + +package client + +import ( + "encoding/json" + "fmt" + + projectservice "github.com/linuxfoundation/lfx-v2-project-service/cmd/project-api/gen/project_service" + goa "goa.design/goa/v3/pkg" +) + +// BuildGetProjectsPayload builds the payload for the project-service +// get-projects endpoint from CLI flags. +func BuildGetProjectsPayload(projectServiceGetProjectsVersion string, projectServiceGetProjectsBearerToken string) (*projectservice.GetProjectsPayload, error) { + var err error + var version *string + { + if projectServiceGetProjectsVersion != "" { + version = &projectServiceGetProjectsVersion + if !(*version == "1") { + err = goa.MergeErrors(err, goa.InvalidEnumValueError("version", *version, []any{"1"})) + } + if err != nil { + return nil, err + } + } + } + var bearerToken *string + { + if projectServiceGetProjectsBearerToken != "" { + bearerToken = &projectServiceGetProjectsBearerToken + } + } + v := &projectservice.GetProjectsPayload{} + v.Version = version + v.BearerToken = bearerToken + + return v, nil +} + +// BuildCreateProjectPayload builds the payload for the project-service +// create-project endpoint from CLI flags. +func BuildCreateProjectPayload(projectServiceCreateProjectBody string, projectServiceCreateProjectVersion string, projectServiceCreateProjectBearerToken string) (*projectservice.CreateProjectPayload, error) { + var err error + var body CreateProjectRequestBody + { + err = json.Unmarshal([]byte(projectServiceCreateProjectBody), &body) + if err != nil { + return nil, fmt.Errorf("invalid JSON for body, \nerror: %s, \nexample of valid JSON:\n%s", err, "'{\n \"auditors\": [\n \"user123\",\n \"user456\"\n ],\n \"description\": \"project foo is a project about bar\",\n \"name\": \"Foo Foundation\",\n \"parent_uid\": \"7cad5a8d-19d0-41a4-81a6-043453daf9ee\",\n \"public\": true,\n \"slug\": \"project-slug\",\n \"writers\": [\n \"user123\",\n \"user456\"\n ]\n }'") + } + err = goa.MergeErrors(err, goa.ValidateFormat("body.slug", body.Slug, goa.FormatRegexp)) + err = goa.MergeErrors(err, goa.ValidatePattern("body.slug", body.Slug, "^[a-z][a-z0-9_\\-]*[a-z0-9]$")) + if err != nil { + return nil, err + } + } + var version *string + { + if projectServiceCreateProjectVersion != "" { + version = &projectServiceCreateProjectVersion + if !(*version == "1") { + err = goa.MergeErrors(err, goa.InvalidEnumValueError("version", *version, []any{"1"})) + } + if err != nil { + return nil, err + } + } + } + var bearerToken *string + { + if projectServiceCreateProjectBearerToken != "" { + bearerToken = &projectServiceCreateProjectBearerToken + } + } + v := &projectservice.CreateProjectPayload{ + Slug: body.Slug, + Description: body.Description, + Name: body.Name, + Public: body.Public, + ParentUID: body.ParentUID, + } + if body.Auditors != nil { + v.Auditors = make([]string, len(body.Auditors)) + for i, val := range body.Auditors { + v.Auditors[i] = val + } + } + if body.Writers != nil { + v.Writers = make([]string, len(body.Writers)) + for i, val := range body.Writers { + v.Writers[i] = val + } + } + v.Version = version + v.BearerToken = bearerToken + + return v, nil +} + +// BuildGetOneProjectPayload builds the payload for the project-service +// get-one-project endpoint from CLI flags. +func BuildGetOneProjectPayload(projectServiceGetOneProjectID string, projectServiceGetOneProjectVersion string, projectServiceGetOneProjectBearerToken string) (*projectservice.GetOneProjectPayload, error) { + var err error + var id string + { + id = projectServiceGetOneProjectID + err = goa.MergeErrors(err, goa.ValidateFormat("id", id, goa.FormatUUID)) + if err != nil { + return nil, err + } + } + var version *string + { + if projectServiceGetOneProjectVersion != "" { + version = &projectServiceGetOneProjectVersion + if !(*version == "1") { + err = goa.MergeErrors(err, goa.InvalidEnumValueError("version", *version, []any{"1"})) + } + if err != nil { + return nil, err + } + } + } + var bearerToken *string + { + if projectServiceGetOneProjectBearerToken != "" { + bearerToken = &projectServiceGetOneProjectBearerToken + } + } + v := &projectservice.GetOneProjectPayload{} + v.ID = &id + v.Version = version + v.BearerToken = bearerToken + + return v, nil +} + +// BuildUpdateProjectPayload builds the payload for the project-service +// update-project endpoint from CLI flags. +func BuildUpdateProjectPayload(projectServiceUpdateProjectBody string, projectServiceUpdateProjectID string, projectServiceUpdateProjectVersion string, projectServiceUpdateProjectBearerToken string, projectServiceUpdateProjectEtag string) (*projectservice.UpdateProjectPayload, error) { + var err error + var body UpdateProjectRequestBody + { + err = json.Unmarshal([]byte(projectServiceUpdateProjectBody), &body) + if err != nil { + return nil, fmt.Errorf("invalid JSON for body, \nerror: %s, \nexample of valid JSON:\n%s", err, "'{\n \"auditors\": [\n \"user123\",\n \"user456\"\n ],\n \"description\": \"project foo is a project about bar\",\n \"name\": \"Foo Foundation\",\n \"parent_uid\": \"7cad5a8d-19d0-41a4-81a6-043453daf9ee\",\n \"public\": true,\n \"slug\": \"project-slug\",\n \"writers\": [\n \"user123\",\n \"user456\"\n ]\n }'") + } + err = goa.MergeErrors(err, goa.ValidateFormat("body.slug", body.Slug, goa.FormatRegexp)) + err = goa.MergeErrors(err, goa.ValidatePattern("body.slug", body.Slug, "^[a-z][a-z0-9_\\-]*[a-z0-9]$")) + if err != nil { + return nil, err + } + } + var id string + { + id = projectServiceUpdateProjectID + err = goa.MergeErrors(err, goa.ValidateFormat("id", id, goa.FormatUUID)) + if err != nil { + return nil, err + } + } + var version *string + { + if projectServiceUpdateProjectVersion != "" { + version = &projectServiceUpdateProjectVersion + if !(*version == "1") { + err = goa.MergeErrors(err, goa.InvalidEnumValueError("version", *version, []any{"1"})) + } + if err != nil { + return nil, err + } + } + } + var bearerToken *string + { + if projectServiceUpdateProjectBearerToken != "" { + bearerToken = &projectServiceUpdateProjectBearerToken + } + } + var etag *string + { + if projectServiceUpdateProjectEtag != "" { + etag = &projectServiceUpdateProjectEtag + } + } + v := &projectservice.UpdateProjectPayload{ + Slug: body.Slug, + Description: body.Description, + Name: body.Name, + Public: body.Public, + ParentUID: body.ParentUID, + } + if body.Auditors != nil { + v.Auditors = make([]string, len(body.Auditors)) + for i, val := range body.Auditors { + v.Auditors[i] = val + } + } + if body.Writers != nil { + v.Writers = make([]string, len(body.Writers)) + for i, val := range body.Writers { + v.Writers[i] = val + } + } + v.ID = &id + v.Version = version + v.BearerToken = bearerToken + v.Etag = etag + + return v, nil +} + +// BuildDeleteProjectPayload builds the payload for the project-service +// delete-project endpoint from CLI flags. +func BuildDeleteProjectPayload(projectServiceDeleteProjectID string, projectServiceDeleteProjectVersion string, projectServiceDeleteProjectBearerToken string, projectServiceDeleteProjectEtag string) (*projectservice.DeleteProjectPayload, error) { + var err error + var id string + { + id = projectServiceDeleteProjectID + err = goa.MergeErrors(err, goa.ValidateFormat("id", id, goa.FormatUUID)) + if err != nil { + return nil, err + } + } + var version *string + { + if projectServiceDeleteProjectVersion != "" { + version = &projectServiceDeleteProjectVersion + if !(*version == "1") { + err = goa.MergeErrors(err, goa.InvalidEnumValueError("version", *version, []any{"1"})) + } + if err != nil { + return nil, err + } + } + } + var bearerToken *string + { + if projectServiceDeleteProjectBearerToken != "" { + bearerToken = &projectServiceDeleteProjectBearerToken + } + } + var etag *string + { + if projectServiceDeleteProjectEtag != "" { + etag = &projectServiceDeleteProjectEtag + } + } + v := &projectservice.DeleteProjectPayload{} + v.ID = &id + v.Version = version + v.BearerToken = bearerToken + v.Etag = etag + + return v, nil +} diff --git a/cmd/project-api/gen/http/project_service/client/client.go b/cmd/project-api/gen/http/project_service/client/client.go new file mode 100644 index 0000000..ef7af26 --- /dev/null +++ b/cmd/project-api/gen/http/project_service/client/client.go @@ -0,0 +1,239 @@ +// Code generated by goa v3.21.1, DO NOT EDIT. +// +// project-service client HTTP transport +// +// Command: +// $ goa gen +// github.com/linuxfoundation/lfx-v2-project-service/cmd/project-api/design + +package client + +import ( + "context" + "net/http" + + goahttp "goa.design/goa/v3/http" + goa "goa.design/goa/v3/pkg" +) + +// Client lists the project-service service endpoint HTTP clients. +type Client struct { + // GetProjects Doer is the HTTP client used to make requests to the + // get-projects endpoint. + GetProjectsDoer goahttp.Doer + + // CreateProject Doer is the HTTP client used to make requests to the + // create-project endpoint. + CreateProjectDoer goahttp.Doer + + // GetOneProject Doer is the HTTP client used to make requests to the + // get-one-project endpoint. + GetOneProjectDoer goahttp.Doer + + // UpdateProject Doer is the HTTP client used to make requests to the + // update-project endpoint. + UpdateProjectDoer goahttp.Doer + + // DeleteProject Doer is the HTTP client used to make requests to the + // delete-project endpoint. + DeleteProjectDoer goahttp.Doer + + // Readyz Doer is the HTTP client used to make requests to the readyz endpoint. + ReadyzDoer goahttp.Doer + + // Livez Doer is the HTTP client used to make requests to the livez endpoint. + LivezDoer goahttp.Doer + + // RestoreResponseBody controls whether the response bodies are reset after + // decoding so they can be read again. + RestoreResponseBody bool + + scheme string + host string + encoder func(*http.Request) goahttp.Encoder + decoder func(*http.Response) goahttp.Decoder +} + +// NewClient instantiates HTTP clients for all the project-service service +// servers. +func NewClient( + scheme string, + host string, + doer goahttp.Doer, + enc func(*http.Request) goahttp.Encoder, + dec func(*http.Response) goahttp.Decoder, + restoreBody bool, +) *Client { + return &Client{ + GetProjectsDoer: doer, + CreateProjectDoer: doer, + GetOneProjectDoer: doer, + UpdateProjectDoer: doer, + DeleteProjectDoer: doer, + ReadyzDoer: doer, + LivezDoer: doer, + RestoreResponseBody: restoreBody, + scheme: scheme, + host: host, + decoder: dec, + encoder: enc, + } +} + +// GetProjects returns an endpoint that makes HTTP requests to the +// project-service service get-projects server. +func (c *Client) GetProjects() goa.Endpoint { + var ( + encodeRequest = EncodeGetProjectsRequest(c.encoder) + decodeResponse = DecodeGetProjectsResponse(c.decoder, c.RestoreResponseBody) + ) + return func(ctx context.Context, v any) (any, error) { + req, err := c.BuildGetProjectsRequest(ctx, v) + if err != nil { + return nil, err + } + err = encodeRequest(req, v) + if err != nil { + return nil, err + } + resp, err := c.GetProjectsDoer.Do(req) + if err != nil { + return nil, goahttp.ErrRequestError("project-service", "get-projects", err) + } + return decodeResponse(resp) + } +} + +// CreateProject returns an endpoint that makes HTTP requests to the +// project-service service create-project server. +func (c *Client) CreateProject() goa.Endpoint { + var ( + encodeRequest = EncodeCreateProjectRequest(c.encoder) + decodeResponse = DecodeCreateProjectResponse(c.decoder, c.RestoreResponseBody) + ) + return func(ctx context.Context, v any) (any, error) { + req, err := c.BuildCreateProjectRequest(ctx, v) + if err != nil { + return nil, err + } + err = encodeRequest(req, v) + if err != nil { + return nil, err + } + resp, err := c.CreateProjectDoer.Do(req) + if err != nil { + return nil, goahttp.ErrRequestError("project-service", "create-project", err) + } + return decodeResponse(resp) + } +} + +// GetOneProject returns an endpoint that makes HTTP requests to the +// project-service service get-one-project server. +func (c *Client) GetOneProject() goa.Endpoint { + var ( + encodeRequest = EncodeGetOneProjectRequest(c.encoder) + decodeResponse = DecodeGetOneProjectResponse(c.decoder, c.RestoreResponseBody) + ) + return func(ctx context.Context, v any) (any, error) { + req, err := c.BuildGetOneProjectRequest(ctx, v) + if err != nil { + return nil, err + } + err = encodeRequest(req, v) + if err != nil { + return nil, err + } + resp, err := c.GetOneProjectDoer.Do(req) + if err != nil { + return nil, goahttp.ErrRequestError("project-service", "get-one-project", err) + } + return decodeResponse(resp) + } +} + +// UpdateProject returns an endpoint that makes HTTP requests to the +// project-service service update-project server. +func (c *Client) UpdateProject() goa.Endpoint { + var ( + encodeRequest = EncodeUpdateProjectRequest(c.encoder) + decodeResponse = DecodeUpdateProjectResponse(c.decoder, c.RestoreResponseBody) + ) + return func(ctx context.Context, v any) (any, error) { + req, err := c.BuildUpdateProjectRequest(ctx, v) + if err != nil { + return nil, err + } + err = encodeRequest(req, v) + if err != nil { + return nil, err + } + resp, err := c.UpdateProjectDoer.Do(req) + if err != nil { + return nil, goahttp.ErrRequestError("project-service", "update-project", err) + } + return decodeResponse(resp) + } +} + +// DeleteProject returns an endpoint that makes HTTP requests to the +// project-service service delete-project server. +func (c *Client) DeleteProject() goa.Endpoint { + var ( + encodeRequest = EncodeDeleteProjectRequest(c.encoder) + decodeResponse = DecodeDeleteProjectResponse(c.decoder, c.RestoreResponseBody) + ) + return func(ctx context.Context, v any) (any, error) { + req, err := c.BuildDeleteProjectRequest(ctx, v) + if err != nil { + return nil, err + } + err = encodeRequest(req, v) + if err != nil { + return nil, err + } + resp, err := c.DeleteProjectDoer.Do(req) + if err != nil { + return nil, goahttp.ErrRequestError("project-service", "delete-project", err) + } + return decodeResponse(resp) + } +} + +// Readyz returns an endpoint that makes HTTP requests to the project-service +// service readyz server. +func (c *Client) Readyz() goa.Endpoint { + var ( + decodeResponse = DecodeReadyzResponse(c.decoder, c.RestoreResponseBody) + ) + return func(ctx context.Context, v any) (any, error) { + req, err := c.BuildReadyzRequest(ctx, v) + if err != nil { + return nil, err + } + resp, err := c.ReadyzDoer.Do(req) + if err != nil { + return nil, goahttp.ErrRequestError("project-service", "readyz", err) + } + return decodeResponse(resp) + } +} + +// Livez returns an endpoint that makes HTTP requests to the project-service +// service livez server. +func (c *Client) Livez() goa.Endpoint { + var ( + decodeResponse = DecodeLivezResponse(c.decoder, c.RestoreResponseBody) + ) + return func(ctx context.Context, v any) (any, error) { + req, err := c.BuildLivezRequest(ctx, v) + if err != nil { + return nil, err + } + resp, err := c.LivezDoer.Do(req) + if err != nil { + return nil, goahttp.ErrRequestError("project-service", "livez", err) + } + return decodeResponse(resp) + } +} diff --git a/cmd/project-api/gen/http/project_service/client/encode_decode.go b/cmd/project-api/gen/http/project_service/client/encode_decode.go new file mode 100644 index 0000000..87732c0 --- /dev/null +++ b/cmd/project-api/gen/http/project_service/client/encode_decode.go @@ -0,0 +1,898 @@ +// Code generated by goa v3.21.1, DO NOT EDIT. +// +// project-service HTTP client encoders and decoders +// +// Command: +// $ goa gen +// github.com/linuxfoundation/lfx-v2-project-service/cmd/project-api/design + +package client + +import ( + "bytes" + "context" + "io" + "net/http" + "net/url" + "strings" + + projectservice "github.com/linuxfoundation/lfx-v2-project-service/cmd/project-api/gen/project_service" + goahttp "goa.design/goa/v3/http" +) + +// BuildGetProjectsRequest instantiates a HTTP request object with method and +// path set to call the "project-service" service "get-projects" endpoint +func (c *Client) BuildGetProjectsRequest(ctx context.Context, v any) (*http.Request, error) { + u := &url.URL{Scheme: c.scheme, Host: c.host, Path: GetProjectsProjectServicePath()} + req, err := http.NewRequest("GET", u.String(), nil) + if err != nil { + return nil, goahttp.ErrInvalidURL("project-service", "get-projects", u.String(), err) + } + if ctx != nil { + req = req.WithContext(ctx) + } + + return req, nil +} + +// EncodeGetProjectsRequest returns an encoder for requests sent to the +// project-service get-projects server. +func EncodeGetProjectsRequest(encoder func(*http.Request) goahttp.Encoder) func(*http.Request, any) error { + return func(req *http.Request, v any) error { + p, ok := v.(*projectservice.GetProjectsPayload) + if !ok { + return goahttp.ErrInvalidType("project-service", "get-projects", "*projectservice.GetProjectsPayload", v) + } + if p.BearerToken != nil { + head := *p.BearerToken + if !strings.Contains(head, " ") { + req.Header.Set("Authorization", "Bearer "+head) + } else { + req.Header.Set("Authorization", head) + } + } + values := req.URL.Query() + if p.Version != nil { + values.Add("v", *p.Version) + } + req.URL.RawQuery = values.Encode() + return nil + } +} + +// DecodeGetProjectsResponse returns a decoder for responses returned by the +// project-service get-projects endpoint. restoreBody controls whether the +// response body should be restored after having been read. +// DecodeGetProjectsResponse may return the following errors: +// - "BadRequest" (type *projectservice.BadRequestError): http.StatusBadRequest +// - "InternalServerError" (type *projectservice.InternalServerError): http.StatusInternalServerError +// - "ServiceUnavailable" (type *projectservice.ServiceUnavailableError): http.StatusServiceUnavailable +// - error: internal error +func DecodeGetProjectsResponse(decoder func(*http.Response) goahttp.Decoder, restoreBody bool) func(*http.Response) (any, error) { + return func(resp *http.Response) (any, error) { + if restoreBody { + b, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + resp.Body = io.NopCloser(bytes.NewBuffer(b)) + defer func() { + resp.Body = io.NopCloser(bytes.NewBuffer(b)) + }() + } else { + defer resp.Body.Close() + } + switch resp.StatusCode { + case http.StatusOK: + var ( + body GetProjectsResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("project-service", "get-projects", err) + } + err = ValidateGetProjectsResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("project-service", "get-projects", err) + } + var ( + cacheControl *string + ) + cacheControlRaw := resp.Header.Get("Cache-Control") + if cacheControlRaw != "" { + cacheControl = &cacheControlRaw + } + res := NewGetProjectsResultOK(&body, cacheControl) + return res, nil + case http.StatusBadRequest: + var ( + body GetProjectsBadRequestResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("project-service", "get-projects", err) + } + err = ValidateGetProjectsBadRequestResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("project-service", "get-projects", err) + } + return nil, NewGetProjectsBadRequest(&body) + case http.StatusInternalServerError: + var ( + body GetProjectsInternalServerErrorResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("project-service", "get-projects", err) + } + err = ValidateGetProjectsInternalServerErrorResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("project-service", "get-projects", err) + } + return nil, NewGetProjectsInternalServerError(&body) + case http.StatusServiceUnavailable: + var ( + body GetProjectsServiceUnavailableResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("project-service", "get-projects", err) + } + err = ValidateGetProjectsServiceUnavailableResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("project-service", "get-projects", err) + } + return nil, NewGetProjectsServiceUnavailable(&body) + default: + body, _ := io.ReadAll(resp.Body) + return nil, goahttp.ErrInvalidResponse("project-service", "get-projects", resp.StatusCode, string(body)) + } + } +} + +// BuildCreateProjectRequest instantiates a HTTP request object with method and +// path set to call the "project-service" service "create-project" endpoint +func (c *Client) BuildCreateProjectRequest(ctx context.Context, v any) (*http.Request, error) { + u := &url.URL{Scheme: c.scheme, Host: c.host, Path: CreateProjectProjectServicePath()} + req, err := http.NewRequest("POST", u.String(), nil) + if err != nil { + return nil, goahttp.ErrInvalidURL("project-service", "create-project", u.String(), err) + } + if ctx != nil { + req = req.WithContext(ctx) + } + + return req, nil +} + +// EncodeCreateProjectRequest returns an encoder for requests sent to the +// project-service create-project server. +func EncodeCreateProjectRequest(encoder func(*http.Request) goahttp.Encoder) func(*http.Request, any) error { + return func(req *http.Request, v any) error { + p, ok := v.(*projectservice.CreateProjectPayload) + if !ok { + return goahttp.ErrInvalidType("project-service", "create-project", "*projectservice.CreateProjectPayload", v) + } + if p.BearerToken != nil { + head := *p.BearerToken + if !strings.Contains(head, " ") { + req.Header.Set("Authorization", "Bearer "+head) + } else { + req.Header.Set("Authorization", head) + } + } + values := req.URL.Query() + if p.Version != nil { + values.Add("v", *p.Version) + } + req.URL.RawQuery = values.Encode() + body := NewCreateProjectRequestBody(p) + if err := encoder(req).Encode(&body); err != nil { + return goahttp.ErrEncodingError("project-service", "create-project", err) + } + return nil + } +} + +// DecodeCreateProjectResponse returns a decoder for responses returned by the +// project-service create-project endpoint. restoreBody controls whether the +// response body should be restored after having been read. +// DecodeCreateProjectResponse may return the following errors: +// - "BadRequest" (type *projectservice.BadRequestError): http.StatusBadRequest +// - "Conflict" (type *projectservice.ConflictError): http.StatusConflict +// - "InternalServerError" (type *projectservice.InternalServerError): http.StatusInternalServerError +// - "ServiceUnavailable" (type *projectservice.ServiceUnavailableError): http.StatusServiceUnavailable +// - error: internal error +func DecodeCreateProjectResponse(decoder func(*http.Response) goahttp.Decoder, restoreBody bool) func(*http.Response) (any, error) { + return func(resp *http.Response) (any, error) { + if restoreBody { + b, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + resp.Body = io.NopCloser(bytes.NewBuffer(b)) + defer func() { + resp.Body = io.NopCloser(bytes.NewBuffer(b)) + }() + } else { + defer resp.Body.Close() + } + switch resp.StatusCode { + case http.StatusCreated: + var ( + body CreateProjectResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("project-service", "create-project", err) + } + err = ValidateCreateProjectResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("project-service", "create-project", err) + } + res := NewCreateProjectProjectCreated(&body) + return res, nil + case http.StatusBadRequest: + var ( + body CreateProjectBadRequestResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("project-service", "create-project", err) + } + err = ValidateCreateProjectBadRequestResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("project-service", "create-project", err) + } + return nil, NewCreateProjectBadRequest(&body) + case http.StatusConflict: + var ( + body CreateProjectConflictResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("project-service", "create-project", err) + } + err = ValidateCreateProjectConflictResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("project-service", "create-project", err) + } + return nil, NewCreateProjectConflict(&body) + case http.StatusInternalServerError: + var ( + body CreateProjectInternalServerErrorResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("project-service", "create-project", err) + } + err = ValidateCreateProjectInternalServerErrorResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("project-service", "create-project", err) + } + return nil, NewCreateProjectInternalServerError(&body) + case http.StatusServiceUnavailable: + var ( + body CreateProjectServiceUnavailableResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("project-service", "create-project", err) + } + err = ValidateCreateProjectServiceUnavailableResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("project-service", "create-project", err) + } + return nil, NewCreateProjectServiceUnavailable(&body) + default: + body, _ := io.ReadAll(resp.Body) + return nil, goahttp.ErrInvalidResponse("project-service", "create-project", resp.StatusCode, string(body)) + } + } +} + +// BuildGetOneProjectRequest instantiates a HTTP request object with method and +// path set to call the "project-service" service "get-one-project" endpoint +func (c *Client) BuildGetOneProjectRequest(ctx context.Context, v any) (*http.Request, error) { + var ( + id string + ) + { + p, ok := v.(*projectservice.GetOneProjectPayload) + if !ok { + return nil, goahttp.ErrInvalidType("project-service", "get-one-project", "*projectservice.GetOneProjectPayload", v) + } + if p.ID != nil { + id = *p.ID + } + } + u := &url.URL{Scheme: c.scheme, Host: c.host, Path: GetOneProjectProjectServicePath(id)} + req, err := http.NewRequest("GET", u.String(), nil) + if err != nil { + return nil, goahttp.ErrInvalidURL("project-service", "get-one-project", u.String(), err) + } + if ctx != nil { + req = req.WithContext(ctx) + } + + return req, nil +} + +// EncodeGetOneProjectRequest returns an encoder for requests sent to the +// project-service get-one-project server. +func EncodeGetOneProjectRequest(encoder func(*http.Request) goahttp.Encoder) func(*http.Request, any) error { + return func(req *http.Request, v any) error { + p, ok := v.(*projectservice.GetOneProjectPayload) + if !ok { + return goahttp.ErrInvalidType("project-service", "get-one-project", "*projectservice.GetOneProjectPayload", v) + } + if p.BearerToken != nil { + head := *p.BearerToken + if !strings.Contains(head, " ") { + req.Header.Set("Authorization", "Bearer "+head) + } else { + req.Header.Set("Authorization", head) + } + } + values := req.URL.Query() + if p.Version != nil { + values.Add("v", *p.Version) + } + req.URL.RawQuery = values.Encode() + return nil + } +} + +// DecodeGetOneProjectResponse returns a decoder for responses returned by the +// project-service get-one-project endpoint. restoreBody controls whether the +// response body should be restored after having been read. +// DecodeGetOneProjectResponse may return the following errors: +// - "InternalServerError" (type *projectservice.InternalServerError): http.StatusInternalServerError +// - "NotFound" (type *projectservice.NotFoundError): http.StatusNotFound +// - "ServiceUnavailable" (type *projectservice.ServiceUnavailableError): http.StatusServiceUnavailable +// - error: internal error +func DecodeGetOneProjectResponse(decoder func(*http.Response) goahttp.Decoder, restoreBody bool) func(*http.Response) (any, error) { + return func(resp *http.Response) (any, error) { + if restoreBody { + b, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + resp.Body = io.NopCloser(bytes.NewBuffer(b)) + defer func() { + resp.Body = io.NopCloser(bytes.NewBuffer(b)) + }() + } else { + defer resp.Body.Close() + } + switch resp.StatusCode { + case http.StatusOK: + var ( + body GetOneProjectResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("project-service", "get-one-project", err) + } + err = ValidateGetOneProjectResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("project-service", "get-one-project", err) + } + var ( + etag *string + ) + etagRaw := resp.Header.Get("Etag") + if etagRaw != "" { + etag = &etagRaw + } + res := NewGetOneProjectResultOK(&body, etag) + return res, nil + case http.StatusInternalServerError: + var ( + body GetOneProjectInternalServerErrorResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("project-service", "get-one-project", err) + } + err = ValidateGetOneProjectInternalServerErrorResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("project-service", "get-one-project", err) + } + return nil, NewGetOneProjectInternalServerError(&body) + case http.StatusNotFound: + var ( + body GetOneProjectNotFoundResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("project-service", "get-one-project", err) + } + err = ValidateGetOneProjectNotFoundResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("project-service", "get-one-project", err) + } + return nil, NewGetOneProjectNotFound(&body) + case http.StatusServiceUnavailable: + var ( + body GetOneProjectServiceUnavailableResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("project-service", "get-one-project", err) + } + err = ValidateGetOneProjectServiceUnavailableResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("project-service", "get-one-project", err) + } + return nil, NewGetOneProjectServiceUnavailable(&body) + default: + body, _ := io.ReadAll(resp.Body) + return nil, goahttp.ErrInvalidResponse("project-service", "get-one-project", resp.StatusCode, string(body)) + } + } +} + +// BuildUpdateProjectRequest instantiates a HTTP request object with method and +// path set to call the "project-service" service "update-project" endpoint +func (c *Client) BuildUpdateProjectRequest(ctx context.Context, v any) (*http.Request, error) { + var ( + id string + ) + { + p, ok := v.(*projectservice.UpdateProjectPayload) + if !ok { + return nil, goahttp.ErrInvalidType("project-service", "update-project", "*projectservice.UpdateProjectPayload", v) + } + if p.ID != nil { + id = *p.ID + } + } + u := &url.URL{Scheme: c.scheme, Host: c.host, Path: UpdateProjectProjectServicePath(id)} + req, err := http.NewRequest("PUT", u.String(), nil) + if err != nil { + return nil, goahttp.ErrInvalidURL("project-service", "update-project", u.String(), err) + } + if ctx != nil { + req = req.WithContext(ctx) + } + + return req, nil +} + +// EncodeUpdateProjectRequest returns an encoder for requests sent to the +// project-service update-project server. +func EncodeUpdateProjectRequest(encoder func(*http.Request) goahttp.Encoder) func(*http.Request, any) error { + return func(req *http.Request, v any) error { + p, ok := v.(*projectservice.UpdateProjectPayload) + if !ok { + return goahttp.ErrInvalidType("project-service", "update-project", "*projectservice.UpdateProjectPayload", v) + } + if p.BearerToken != nil { + head := *p.BearerToken + if !strings.Contains(head, " ") { + req.Header.Set("Authorization", "Bearer "+head) + } else { + req.Header.Set("Authorization", head) + } + } + if p.Etag != nil { + head := *p.Etag + req.Header.Set("ETag", head) + } + values := req.URL.Query() + if p.Version != nil { + values.Add("v", *p.Version) + } + req.URL.RawQuery = values.Encode() + body := NewUpdateProjectRequestBody(p) + if err := encoder(req).Encode(&body); err != nil { + return goahttp.ErrEncodingError("project-service", "update-project", err) + } + return nil + } +} + +// DecodeUpdateProjectResponse returns a decoder for responses returned by the +// project-service update-project endpoint. restoreBody controls whether the +// response body should be restored after having been read. +// DecodeUpdateProjectResponse may return the following errors: +// - "BadRequest" (type *projectservice.BadRequestError): http.StatusBadRequest +// - "InternalServerError" (type *projectservice.InternalServerError): http.StatusInternalServerError +// - "NotFound" (type *projectservice.NotFoundError): http.StatusNotFound +// - "ServiceUnavailable" (type *projectservice.ServiceUnavailableError): http.StatusServiceUnavailable +// - error: internal error +func DecodeUpdateProjectResponse(decoder func(*http.Response) goahttp.Decoder, restoreBody bool) func(*http.Response) (any, error) { + return func(resp *http.Response) (any, error) { + if restoreBody { + b, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + resp.Body = io.NopCloser(bytes.NewBuffer(b)) + defer func() { + resp.Body = io.NopCloser(bytes.NewBuffer(b)) + }() + } else { + defer resp.Body.Close() + } + switch resp.StatusCode { + case http.StatusOK: + var ( + body UpdateProjectResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("project-service", "update-project", err) + } + err = ValidateUpdateProjectResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("project-service", "update-project", err) + } + res := NewUpdateProjectProjectOK(&body) + return res, nil + case http.StatusBadRequest: + var ( + body UpdateProjectBadRequestResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("project-service", "update-project", err) + } + err = ValidateUpdateProjectBadRequestResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("project-service", "update-project", err) + } + return nil, NewUpdateProjectBadRequest(&body) + case http.StatusInternalServerError: + var ( + body UpdateProjectInternalServerErrorResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("project-service", "update-project", err) + } + err = ValidateUpdateProjectInternalServerErrorResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("project-service", "update-project", err) + } + return nil, NewUpdateProjectInternalServerError(&body) + case http.StatusNotFound: + var ( + body UpdateProjectNotFoundResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("project-service", "update-project", err) + } + err = ValidateUpdateProjectNotFoundResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("project-service", "update-project", err) + } + return nil, NewUpdateProjectNotFound(&body) + case http.StatusServiceUnavailable: + var ( + body UpdateProjectServiceUnavailableResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("project-service", "update-project", err) + } + err = ValidateUpdateProjectServiceUnavailableResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("project-service", "update-project", err) + } + return nil, NewUpdateProjectServiceUnavailable(&body) + default: + body, _ := io.ReadAll(resp.Body) + return nil, goahttp.ErrInvalidResponse("project-service", "update-project", resp.StatusCode, string(body)) + } + } +} + +// BuildDeleteProjectRequest instantiates a HTTP request object with method and +// path set to call the "project-service" service "delete-project" endpoint +func (c *Client) BuildDeleteProjectRequest(ctx context.Context, v any) (*http.Request, error) { + var ( + id string + ) + { + p, ok := v.(*projectservice.DeleteProjectPayload) + if !ok { + return nil, goahttp.ErrInvalidType("project-service", "delete-project", "*projectservice.DeleteProjectPayload", v) + } + if p.ID != nil { + id = *p.ID + } + } + u := &url.URL{Scheme: c.scheme, Host: c.host, Path: DeleteProjectProjectServicePath(id)} + req, err := http.NewRequest("DELETE", u.String(), nil) + if err != nil { + return nil, goahttp.ErrInvalidURL("project-service", "delete-project", u.String(), err) + } + if ctx != nil { + req = req.WithContext(ctx) + } + + return req, nil +} + +// EncodeDeleteProjectRequest returns an encoder for requests sent to the +// project-service delete-project server. +func EncodeDeleteProjectRequest(encoder func(*http.Request) goahttp.Encoder) func(*http.Request, any) error { + return func(req *http.Request, v any) error { + p, ok := v.(*projectservice.DeleteProjectPayload) + if !ok { + return goahttp.ErrInvalidType("project-service", "delete-project", "*projectservice.DeleteProjectPayload", v) + } + if p.BearerToken != nil { + head := *p.BearerToken + if !strings.Contains(head, " ") { + req.Header.Set("Authorization", "Bearer "+head) + } else { + req.Header.Set("Authorization", head) + } + } + if p.Etag != nil { + head := *p.Etag + req.Header.Set("ETag", head) + } + values := req.URL.Query() + if p.Version != nil { + values.Add("v", *p.Version) + } + req.URL.RawQuery = values.Encode() + return nil + } +} + +// DecodeDeleteProjectResponse returns a decoder for responses returned by the +// project-service delete-project endpoint. restoreBody controls whether the +// response body should be restored after having been read. +// DecodeDeleteProjectResponse may return the following errors: +// - "BadRequest" (type *projectservice.BadRequestError): http.StatusBadRequest +// - "InternalServerError" (type *projectservice.InternalServerError): http.StatusInternalServerError +// - "NotFound" (type *projectservice.NotFoundError): http.StatusNotFound +// - "ServiceUnavailable" (type *projectservice.ServiceUnavailableError): http.StatusServiceUnavailable +// - error: internal error +func DecodeDeleteProjectResponse(decoder func(*http.Response) goahttp.Decoder, restoreBody bool) func(*http.Response) (any, error) { + return func(resp *http.Response) (any, error) { + if restoreBody { + b, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + resp.Body = io.NopCloser(bytes.NewBuffer(b)) + defer func() { + resp.Body = io.NopCloser(bytes.NewBuffer(b)) + }() + } else { + defer resp.Body.Close() + } + switch resp.StatusCode { + case http.StatusNoContent: + return nil, nil + case http.StatusBadRequest: + var ( + body DeleteProjectBadRequestResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("project-service", "delete-project", err) + } + err = ValidateDeleteProjectBadRequestResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("project-service", "delete-project", err) + } + return nil, NewDeleteProjectBadRequest(&body) + case http.StatusInternalServerError: + var ( + body DeleteProjectInternalServerErrorResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("project-service", "delete-project", err) + } + err = ValidateDeleteProjectInternalServerErrorResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("project-service", "delete-project", err) + } + return nil, NewDeleteProjectInternalServerError(&body) + case http.StatusNotFound: + var ( + body DeleteProjectNotFoundResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("project-service", "delete-project", err) + } + err = ValidateDeleteProjectNotFoundResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("project-service", "delete-project", err) + } + return nil, NewDeleteProjectNotFound(&body) + case http.StatusServiceUnavailable: + var ( + body DeleteProjectServiceUnavailableResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("project-service", "delete-project", err) + } + err = ValidateDeleteProjectServiceUnavailableResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("project-service", "delete-project", err) + } + return nil, NewDeleteProjectServiceUnavailable(&body) + default: + body, _ := io.ReadAll(resp.Body) + return nil, goahttp.ErrInvalidResponse("project-service", "delete-project", resp.StatusCode, string(body)) + } + } +} + +// BuildReadyzRequest instantiates a HTTP request object with method and path +// set to call the "project-service" service "readyz" endpoint +func (c *Client) BuildReadyzRequest(ctx context.Context, v any) (*http.Request, error) { + u := &url.URL{Scheme: c.scheme, Host: c.host, Path: ReadyzProjectServicePath()} + req, err := http.NewRequest("GET", u.String(), nil) + if err != nil { + return nil, goahttp.ErrInvalidURL("project-service", "readyz", u.String(), err) + } + if ctx != nil { + req = req.WithContext(ctx) + } + + return req, nil +} + +// DecodeReadyzResponse returns a decoder for responses returned by the +// project-service readyz endpoint. restoreBody controls whether the response +// body should be restored after having been read. +// DecodeReadyzResponse may return the following errors: +// - "ServiceUnavailable" (type *projectservice.ServiceUnavailableError): http.StatusServiceUnavailable +// - error: internal error +func DecodeReadyzResponse(decoder func(*http.Response) goahttp.Decoder, restoreBody bool) func(*http.Response) (any, error) { + return func(resp *http.Response) (any, error) { + if restoreBody { + b, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + resp.Body = io.NopCloser(bytes.NewBuffer(b)) + defer func() { + resp.Body = io.NopCloser(bytes.NewBuffer(b)) + }() + } else { + defer resp.Body.Close() + } + switch resp.StatusCode { + case http.StatusOK: + var ( + body []byte + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("project-service", "readyz", err) + } + return body, nil + case http.StatusServiceUnavailable: + var ( + body ReadyzServiceUnavailableResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("project-service", "readyz", err) + } + err = ValidateReadyzServiceUnavailableResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("project-service", "readyz", err) + } + return nil, NewReadyzServiceUnavailable(&body) + default: + body, _ := io.ReadAll(resp.Body) + return nil, goahttp.ErrInvalidResponse("project-service", "readyz", resp.StatusCode, string(body)) + } + } +} + +// BuildLivezRequest instantiates a HTTP request object with method and path +// set to call the "project-service" service "livez" endpoint +func (c *Client) BuildLivezRequest(ctx context.Context, v any) (*http.Request, error) { + u := &url.URL{Scheme: c.scheme, Host: c.host, Path: LivezProjectServicePath()} + req, err := http.NewRequest("GET", u.String(), nil) + if err != nil { + return nil, goahttp.ErrInvalidURL("project-service", "livez", u.String(), err) + } + if ctx != nil { + req = req.WithContext(ctx) + } + + return req, nil +} + +// DecodeLivezResponse returns a decoder for responses returned by the +// project-service livez endpoint. restoreBody controls whether the response +// body should be restored after having been read. +func DecodeLivezResponse(decoder func(*http.Response) goahttp.Decoder, restoreBody bool) func(*http.Response) (any, error) { + return func(resp *http.Response) (any, error) { + if restoreBody { + b, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + resp.Body = io.NopCloser(bytes.NewBuffer(b)) + defer func() { + resp.Body = io.NopCloser(bytes.NewBuffer(b)) + }() + } else { + defer resp.Body.Close() + } + switch resp.StatusCode { + case http.StatusOK: + var ( + body []byte + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("project-service", "livez", err) + } + return body, nil + default: + body, _ := io.ReadAll(resp.Body) + return nil, goahttp.ErrInvalidResponse("project-service", "livez", resp.StatusCode, string(body)) + } + } +} + +// unmarshalProjectResponseBodyToProjectserviceProject builds a value of type +// *projectservice.Project from a value of type *ProjectResponseBody. +func unmarshalProjectResponseBodyToProjectserviceProject(v *ProjectResponseBody) *projectservice.Project { + res := &projectservice.Project{ + ID: v.ID, + Slug: v.Slug, + Description: v.Description, + Name: v.Name, + Public: v.Public, + ParentUID: v.ParentUID, + } + if v.Auditors != nil { + res.Auditors = make([]string, len(v.Auditors)) + for i, val := range v.Auditors { + res.Auditors[i] = val + } + } + if v.Writers != nil { + res.Writers = make([]string, len(v.Writers)) + for i, val := range v.Writers { + res.Writers[i] = val + } + } + + return res +} diff --git a/cmd/project-api/gen/http/project_service/client/paths.go b/cmd/project-api/gen/http/project_service/client/paths.go new file mode 100644 index 0000000..b4459d3 --- /dev/null +++ b/cmd/project-api/gen/http/project_service/client/paths.go @@ -0,0 +1,48 @@ +// Code generated by goa v3.21.1, DO NOT EDIT. +// +// HTTP request path constructors for the project-service service. +// +// Command: +// $ goa gen +// github.com/linuxfoundation/lfx-v2-project-service/cmd/project-api/design + +package client + +import ( + "fmt" +) + +// GetProjectsProjectServicePath returns the URL path to the project-service service get-projects HTTP endpoint. +func GetProjectsProjectServicePath() string { + return "/projects" +} + +// CreateProjectProjectServicePath returns the URL path to the project-service service create-project HTTP endpoint. +func CreateProjectProjectServicePath() string { + return "/projects" +} + +// GetOneProjectProjectServicePath returns the URL path to the project-service service get-one-project HTTP endpoint. +func GetOneProjectProjectServicePath(id string) string { + return fmt.Sprintf("/projects/%v", id) +} + +// UpdateProjectProjectServicePath returns the URL path to the project-service service update-project HTTP endpoint. +func UpdateProjectProjectServicePath(id string) string { + return fmt.Sprintf("/projects/%v", id) +} + +// DeleteProjectProjectServicePath returns the URL path to the project-service service delete-project HTTP endpoint. +func DeleteProjectProjectServicePath(id string) string { + return fmt.Sprintf("/projects/%v", id) +} + +// ReadyzProjectServicePath returns the URL path to the project-service service readyz HTTP endpoint. +func ReadyzProjectServicePath() string { + return "/readyz" +} + +// LivezProjectServicePath returns the URL path to the project-service service livez HTTP endpoint. +func LivezProjectServicePath() string { + return "/livez" +} diff --git a/cmd/project-api/gen/http/project_service/client/types.go b/cmd/project-api/gen/http/project_service/client/types.go new file mode 100644 index 0000000..8202420 --- /dev/null +++ b/cmd/project-api/gen/http/project_service/client/types.go @@ -0,0 +1,976 @@ +// Code generated by goa v3.21.1, DO NOT EDIT. +// +// project-service HTTP client types +// +// Command: +// $ goa gen +// github.com/linuxfoundation/lfx-v2-project-service/cmd/project-api/design + +package client + +import ( + projectservice "github.com/linuxfoundation/lfx-v2-project-service/cmd/project-api/gen/project_service" + goa "goa.design/goa/v3/pkg" +) + +// CreateProjectRequestBody is the type of the "project-service" service +// "create-project" endpoint HTTP request body. +type CreateProjectRequestBody struct { + // Project slug, a short slugified name of the project + Slug string `form:"slug" json:"slug" xml:"slug"` + // A description of the project + Description string `form:"description" json:"description" xml:"description"` + // The pretty name of the project + Name string `form:"name" json:"name" xml:"name"` + // Whether the project is public + Public *bool `form:"public,omitempty" json:"public,omitempty" xml:"public,omitempty"` + // The UID of the parent project, should be empty if there is none + ParentUID *string `form:"parent_uid,omitempty" json:"parent_uid,omitempty" xml:"parent_uid,omitempty"` + // A list of project auditors by their user IDs + Auditors []string `form:"auditors,omitempty" json:"auditors,omitempty" xml:"auditors,omitempty"` + // A list of project writers by their user IDs + Writers []string `form:"writers,omitempty" json:"writers,omitempty" xml:"writers,omitempty"` +} + +// UpdateProjectRequestBody is the type of the "project-service" service +// "update-project" endpoint HTTP request body. +type UpdateProjectRequestBody struct { + // Project slug, a short slugified name of the project + Slug string `form:"slug" json:"slug" xml:"slug"` + // A description of the project + Description string `form:"description" json:"description" xml:"description"` + // The pretty name of the project + Name string `form:"name" json:"name" xml:"name"` + // Whether the project is public + Public *bool `form:"public,omitempty" json:"public,omitempty" xml:"public,omitempty"` + // The UID of the parent project, should be empty if there is none + ParentUID *string `form:"parent_uid,omitempty" json:"parent_uid,omitempty" xml:"parent_uid,omitempty"` + // A list of project auditors by their user IDs + Auditors []string `form:"auditors,omitempty" json:"auditors,omitempty" xml:"auditors,omitempty"` + // A list of project writers by their user IDs + Writers []string `form:"writers,omitempty" json:"writers,omitempty" xml:"writers,omitempty"` +} + +// GetProjectsResponseBody is the type of the "project-service" service +// "get-projects" endpoint HTTP response body. +type GetProjectsResponseBody struct { + // Resources found + Projects []*ProjectResponseBody `form:"projects,omitempty" json:"projects,omitempty" xml:"projects,omitempty"` +} + +// CreateProjectResponseBody is the type of the "project-service" service +// "create-project" endpoint HTTP response body. +type CreateProjectResponseBody struct { + // Project ID -- v2 id, not related to v1 id directly + ID *string `form:"id,omitempty" json:"id,omitempty" xml:"id,omitempty"` + // Project slug, a short slugified name of the project + Slug *string `form:"slug,omitempty" json:"slug,omitempty" xml:"slug,omitempty"` + // A description of the project + Description *string `form:"description,omitempty" json:"description,omitempty" xml:"description,omitempty"` + // The pretty name of the project + Name *string `form:"name,omitempty" json:"name,omitempty" xml:"name,omitempty"` + // Whether the project is public + Public *bool `form:"public,omitempty" json:"public,omitempty" xml:"public,omitempty"` + // The UID of the parent project, should be empty if there is none + ParentUID *string `form:"parent_uid,omitempty" json:"parent_uid,omitempty" xml:"parent_uid,omitempty"` + // A list of project auditors by their user IDs + Auditors []string `form:"auditors,omitempty" json:"auditors,omitempty" xml:"auditors,omitempty"` + // A list of project writers by their user IDs + Writers []string `form:"writers,omitempty" json:"writers,omitempty" xml:"writers,omitempty"` +} + +// GetOneProjectResponseBody is the type of the "project-service" service +// "get-one-project" endpoint HTTP response body. +type GetOneProjectResponseBody ProjectResponseBody + +// UpdateProjectResponseBody is the type of the "project-service" service +// "update-project" endpoint HTTP response body. +type UpdateProjectResponseBody struct { + // Project ID -- v2 id, not related to v1 id directly + ID *string `form:"id,omitempty" json:"id,omitempty" xml:"id,omitempty"` + // Project slug, a short slugified name of the project + Slug *string `form:"slug,omitempty" json:"slug,omitempty" xml:"slug,omitempty"` + // A description of the project + Description *string `form:"description,omitempty" json:"description,omitempty" xml:"description,omitempty"` + // The pretty name of the project + Name *string `form:"name,omitempty" json:"name,omitempty" xml:"name,omitempty"` + // Whether the project is public + Public *bool `form:"public,omitempty" json:"public,omitempty" xml:"public,omitempty"` + // The UID of the parent project, should be empty if there is none + ParentUID *string `form:"parent_uid,omitempty" json:"parent_uid,omitempty" xml:"parent_uid,omitempty"` + // A list of project auditors by their user IDs + Auditors []string `form:"auditors,omitempty" json:"auditors,omitempty" xml:"auditors,omitempty"` + // A list of project writers by their user IDs + Writers []string `form:"writers,omitempty" json:"writers,omitempty" xml:"writers,omitempty"` +} + +// GetProjectsBadRequestResponseBody is the type of the "project-service" +// service "get-projects" endpoint HTTP response body for the "BadRequest" +// error. +type GetProjectsBadRequestResponseBody struct { + // HTTP status code + Code *string `form:"code,omitempty" json:"code,omitempty" xml:"code,omitempty"` + // Error message + Message *string `form:"message,omitempty" json:"message,omitempty" xml:"message,omitempty"` +} + +// GetProjectsInternalServerErrorResponseBody is the type of the +// "project-service" service "get-projects" endpoint HTTP response body for the +// "InternalServerError" error. +type GetProjectsInternalServerErrorResponseBody struct { + // HTTP status code + Code *string `form:"code,omitempty" json:"code,omitempty" xml:"code,omitempty"` + // Error message + Message *string `form:"message,omitempty" json:"message,omitempty" xml:"message,omitempty"` +} + +// GetProjectsServiceUnavailableResponseBody is the type of the +// "project-service" service "get-projects" endpoint HTTP response body for the +// "ServiceUnavailable" error. +type GetProjectsServiceUnavailableResponseBody struct { + // HTTP status code + Code *string `form:"code,omitempty" json:"code,omitempty" xml:"code,omitempty"` + // Error message + Message *string `form:"message,omitempty" json:"message,omitempty" xml:"message,omitempty"` +} + +// CreateProjectBadRequestResponseBody is the type of the "project-service" +// service "create-project" endpoint HTTP response body for the "BadRequest" +// error. +type CreateProjectBadRequestResponseBody struct { + // HTTP status code + Code *string `form:"code,omitempty" json:"code,omitempty" xml:"code,omitempty"` + // Error message + Message *string `form:"message,omitempty" json:"message,omitempty" xml:"message,omitempty"` +} + +// CreateProjectConflictResponseBody is the type of the "project-service" +// service "create-project" endpoint HTTP response body for the "Conflict" +// error. +type CreateProjectConflictResponseBody struct { + // HTTP status code + Code *string `form:"code,omitempty" json:"code,omitempty" xml:"code,omitempty"` + // Error message + Message *string `form:"message,omitempty" json:"message,omitempty" xml:"message,omitempty"` +} + +// CreateProjectInternalServerErrorResponseBody is the type of the +// "project-service" service "create-project" endpoint HTTP response body for +// the "InternalServerError" error. +type CreateProjectInternalServerErrorResponseBody struct { + // HTTP status code + Code *string `form:"code,omitempty" json:"code,omitempty" xml:"code,omitempty"` + // Error message + Message *string `form:"message,omitempty" json:"message,omitempty" xml:"message,omitempty"` +} + +// CreateProjectServiceUnavailableResponseBody is the type of the +// "project-service" service "create-project" endpoint HTTP response body for +// the "ServiceUnavailable" error. +type CreateProjectServiceUnavailableResponseBody struct { + // HTTP status code + Code *string `form:"code,omitempty" json:"code,omitempty" xml:"code,omitempty"` + // Error message + Message *string `form:"message,omitempty" json:"message,omitempty" xml:"message,omitempty"` +} + +// GetOneProjectInternalServerErrorResponseBody is the type of the +// "project-service" service "get-one-project" endpoint HTTP response body for +// the "InternalServerError" error. +type GetOneProjectInternalServerErrorResponseBody struct { + // HTTP status code + Code *string `form:"code,omitempty" json:"code,omitempty" xml:"code,omitempty"` + // Error message + Message *string `form:"message,omitempty" json:"message,omitempty" xml:"message,omitempty"` +} + +// GetOneProjectNotFoundResponseBody is the type of the "project-service" +// service "get-one-project" endpoint HTTP response body for the "NotFound" +// error. +type GetOneProjectNotFoundResponseBody struct { + // HTTP status code + Code *string `form:"code,omitempty" json:"code,omitempty" xml:"code,omitempty"` + // Error message + Message *string `form:"message,omitempty" json:"message,omitempty" xml:"message,omitempty"` +} + +// GetOneProjectServiceUnavailableResponseBody is the type of the +// "project-service" service "get-one-project" endpoint HTTP response body for +// the "ServiceUnavailable" error. +type GetOneProjectServiceUnavailableResponseBody struct { + // HTTP status code + Code *string `form:"code,omitempty" json:"code,omitempty" xml:"code,omitempty"` + // Error message + Message *string `form:"message,omitempty" json:"message,omitempty" xml:"message,omitempty"` +} + +// UpdateProjectBadRequestResponseBody is the type of the "project-service" +// service "update-project" endpoint HTTP response body for the "BadRequest" +// error. +type UpdateProjectBadRequestResponseBody struct { + // HTTP status code + Code *string `form:"code,omitempty" json:"code,omitempty" xml:"code,omitempty"` + // Error message + Message *string `form:"message,omitempty" json:"message,omitempty" xml:"message,omitempty"` +} + +// UpdateProjectInternalServerErrorResponseBody is the type of the +// "project-service" service "update-project" endpoint HTTP response body for +// the "InternalServerError" error. +type UpdateProjectInternalServerErrorResponseBody struct { + // HTTP status code + Code *string `form:"code,omitempty" json:"code,omitempty" xml:"code,omitempty"` + // Error message + Message *string `form:"message,omitempty" json:"message,omitempty" xml:"message,omitempty"` +} + +// UpdateProjectNotFoundResponseBody is the type of the "project-service" +// service "update-project" endpoint HTTP response body for the "NotFound" +// error. +type UpdateProjectNotFoundResponseBody struct { + // HTTP status code + Code *string `form:"code,omitempty" json:"code,omitempty" xml:"code,omitempty"` + // Error message + Message *string `form:"message,omitempty" json:"message,omitempty" xml:"message,omitempty"` +} + +// UpdateProjectServiceUnavailableResponseBody is the type of the +// "project-service" service "update-project" endpoint HTTP response body for +// the "ServiceUnavailable" error. +type UpdateProjectServiceUnavailableResponseBody struct { + // HTTP status code + Code *string `form:"code,omitempty" json:"code,omitempty" xml:"code,omitempty"` + // Error message + Message *string `form:"message,omitempty" json:"message,omitempty" xml:"message,omitempty"` +} + +// DeleteProjectBadRequestResponseBody is the type of the "project-service" +// service "delete-project" endpoint HTTP response body for the "BadRequest" +// error. +type DeleteProjectBadRequestResponseBody struct { + // HTTP status code + Code *string `form:"code,omitempty" json:"code,omitempty" xml:"code,omitempty"` + // Error message + Message *string `form:"message,omitempty" json:"message,omitempty" xml:"message,omitempty"` +} + +// DeleteProjectInternalServerErrorResponseBody is the type of the +// "project-service" service "delete-project" endpoint HTTP response body for +// the "InternalServerError" error. +type DeleteProjectInternalServerErrorResponseBody struct { + // HTTP status code + Code *string `form:"code,omitempty" json:"code,omitempty" xml:"code,omitempty"` + // Error message + Message *string `form:"message,omitempty" json:"message,omitempty" xml:"message,omitempty"` +} + +// DeleteProjectNotFoundResponseBody is the type of the "project-service" +// service "delete-project" endpoint HTTP response body for the "NotFound" +// error. +type DeleteProjectNotFoundResponseBody struct { + // HTTP status code + Code *string `form:"code,omitempty" json:"code,omitempty" xml:"code,omitempty"` + // Error message + Message *string `form:"message,omitempty" json:"message,omitempty" xml:"message,omitempty"` +} + +// DeleteProjectServiceUnavailableResponseBody is the type of the +// "project-service" service "delete-project" endpoint HTTP response body for +// the "ServiceUnavailable" error. +type DeleteProjectServiceUnavailableResponseBody struct { + // HTTP status code + Code *string `form:"code,omitempty" json:"code,omitempty" xml:"code,omitempty"` + // Error message + Message *string `form:"message,omitempty" json:"message,omitempty" xml:"message,omitempty"` +} + +// ReadyzServiceUnavailableResponseBody is the type of the "project-service" +// service "readyz" endpoint HTTP response body for the "ServiceUnavailable" +// error. +type ReadyzServiceUnavailableResponseBody struct { + // HTTP status code + Code *string `form:"code,omitempty" json:"code,omitempty" xml:"code,omitempty"` + // Error message + Message *string `form:"message,omitempty" json:"message,omitempty" xml:"message,omitempty"` +} + +// ProjectResponseBody is used to define fields on response body types. +type ProjectResponseBody struct { + // Project ID -- v2 id, not related to v1 id directly + ID *string `form:"id,omitempty" json:"id,omitempty" xml:"id,omitempty"` + // Project slug, a short slugified name of the project + Slug *string `form:"slug,omitempty" json:"slug,omitempty" xml:"slug,omitempty"` + // A description of the project + Description *string `form:"description,omitempty" json:"description,omitempty" xml:"description,omitempty"` + // The pretty name of the project + Name *string `form:"name,omitempty" json:"name,omitempty" xml:"name,omitempty"` + // Whether the project is public + Public *bool `form:"public,omitempty" json:"public,omitempty" xml:"public,omitempty"` + // The UID of the parent project, should be empty if there is none + ParentUID *string `form:"parent_uid,omitempty" json:"parent_uid,omitempty" xml:"parent_uid,omitempty"` + // A list of project auditors by their user IDs + Auditors []string `form:"auditors,omitempty" json:"auditors,omitempty" xml:"auditors,omitempty"` + // A list of project writers by their user IDs + Writers []string `form:"writers,omitempty" json:"writers,omitempty" xml:"writers,omitempty"` +} + +// NewCreateProjectRequestBody builds the HTTP request body from the payload of +// the "create-project" endpoint of the "project-service" service. +func NewCreateProjectRequestBody(p *projectservice.CreateProjectPayload) *CreateProjectRequestBody { + body := &CreateProjectRequestBody{ + Slug: p.Slug, + Description: p.Description, + Name: p.Name, + Public: p.Public, + ParentUID: p.ParentUID, + } + if p.Auditors != nil { + body.Auditors = make([]string, len(p.Auditors)) + for i, val := range p.Auditors { + body.Auditors[i] = val + } + } + if p.Writers != nil { + body.Writers = make([]string, len(p.Writers)) + for i, val := range p.Writers { + body.Writers[i] = val + } + } + return body +} + +// NewUpdateProjectRequestBody builds the HTTP request body from the payload of +// the "update-project" endpoint of the "project-service" service. +func NewUpdateProjectRequestBody(p *projectservice.UpdateProjectPayload) *UpdateProjectRequestBody { + body := &UpdateProjectRequestBody{ + Slug: p.Slug, + Description: p.Description, + Name: p.Name, + Public: p.Public, + ParentUID: p.ParentUID, + } + if p.Auditors != nil { + body.Auditors = make([]string, len(p.Auditors)) + for i, val := range p.Auditors { + body.Auditors[i] = val + } + } + if p.Writers != nil { + body.Writers = make([]string, len(p.Writers)) + for i, val := range p.Writers { + body.Writers[i] = val + } + } + return body +} + +// NewGetProjectsResultOK builds a "project-service" service "get-projects" +// endpoint result from a HTTP "OK" response. +func NewGetProjectsResultOK(body *GetProjectsResponseBody, cacheControl *string) *projectservice.GetProjectsResult { + v := &projectservice.GetProjectsResult{} + v.Projects = make([]*projectservice.Project, len(body.Projects)) + for i, val := range body.Projects { + v.Projects[i] = unmarshalProjectResponseBodyToProjectserviceProject(val) + } + v.CacheControl = cacheControl + + return v +} + +// NewGetProjectsBadRequest builds a project-service service get-projects +// endpoint BadRequest error. +func NewGetProjectsBadRequest(body *GetProjectsBadRequestResponseBody) *projectservice.BadRequestError { + v := &projectservice.BadRequestError{ + Code: *body.Code, + Message: *body.Message, + } + + return v +} + +// NewGetProjectsInternalServerError builds a project-service service +// get-projects endpoint InternalServerError error. +func NewGetProjectsInternalServerError(body *GetProjectsInternalServerErrorResponseBody) *projectservice.InternalServerError { + v := &projectservice.InternalServerError{ + Code: *body.Code, + Message: *body.Message, + } + + return v +} + +// NewGetProjectsServiceUnavailable builds a project-service service +// get-projects endpoint ServiceUnavailable error. +func NewGetProjectsServiceUnavailable(body *GetProjectsServiceUnavailableResponseBody) *projectservice.ServiceUnavailableError { + v := &projectservice.ServiceUnavailableError{ + Code: *body.Code, + Message: *body.Message, + } + + return v +} + +// NewCreateProjectProjectCreated builds a "project-service" service +// "create-project" endpoint result from a HTTP "Created" response. +func NewCreateProjectProjectCreated(body *CreateProjectResponseBody) *projectservice.Project { + v := &projectservice.Project{ + ID: body.ID, + Slug: body.Slug, + Description: body.Description, + Name: body.Name, + Public: body.Public, + ParentUID: body.ParentUID, + } + if body.Auditors != nil { + v.Auditors = make([]string, len(body.Auditors)) + for i, val := range body.Auditors { + v.Auditors[i] = val + } + } + if body.Writers != nil { + v.Writers = make([]string, len(body.Writers)) + for i, val := range body.Writers { + v.Writers[i] = val + } + } + + return v +} + +// NewCreateProjectBadRequest builds a project-service service create-project +// endpoint BadRequest error. +func NewCreateProjectBadRequest(body *CreateProjectBadRequestResponseBody) *projectservice.BadRequestError { + v := &projectservice.BadRequestError{ + Code: *body.Code, + Message: *body.Message, + } + + return v +} + +// NewCreateProjectConflict builds a project-service service create-project +// endpoint Conflict error. +func NewCreateProjectConflict(body *CreateProjectConflictResponseBody) *projectservice.ConflictError { + v := &projectservice.ConflictError{ + Code: *body.Code, + Message: *body.Message, + } + + return v +} + +// NewCreateProjectInternalServerError builds a project-service service +// create-project endpoint InternalServerError error. +func NewCreateProjectInternalServerError(body *CreateProjectInternalServerErrorResponseBody) *projectservice.InternalServerError { + v := &projectservice.InternalServerError{ + Code: *body.Code, + Message: *body.Message, + } + + return v +} + +// NewCreateProjectServiceUnavailable builds a project-service service +// create-project endpoint ServiceUnavailable error. +func NewCreateProjectServiceUnavailable(body *CreateProjectServiceUnavailableResponseBody) *projectservice.ServiceUnavailableError { + v := &projectservice.ServiceUnavailableError{ + Code: *body.Code, + Message: *body.Message, + } + + return v +} + +// NewGetOneProjectResultOK builds a "project-service" service +// "get-one-project" endpoint result from a HTTP "OK" response. +func NewGetOneProjectResultOK(body *GetOneProjectResponseBody, etag *string) *projectservice.GetOneProjectResult { + v := &projectservice.Project{ + ID: body.ID, + Slug: body.Slug, + Description: body.Description, + Name: body.Name, + Public: body.Public, + ParentUID: body.ParentUID, + } + if body.Auditors != nil { + v.Auditors = make([]string, len(body.Auditors)) + for i, val := range body.Auditors { + v.Auditors[i] = val + } + } + if body.Writers != nil { + v.Writers = make([]string, len(body.Writers)) + for i, val := range body.Writers { + v.Writers[i] = val + } + } + res := &projectservice.GetOneProjectResult{ + Project: v, + } + res.Etag = etag + + return res +} + +// NewGetOneProjectInternalServerError builds a project-service service +// get-one-project endpoint InternalServerError error. +func NewGetOneProjectInternalServerError(body *GetOneProjectInternalServerErrorResponseBody) *projectservice.InternalServerError { + v := &projectservice.InternalServerError{ + Code: *body.Code, + Message: *body.Message, + } + + return v +} + +// NewGetOneProjectNotFound builds a project-service service get-one-project +// endpoint NotFound error. +func NewGetOneProjectNotFound(body *GetOneProjectNotFoundResponseBody) *projectservice.NotFoundError { + v := &projectservice.NotFoundError{ + Code: *body.Code, + Message: *body.Message, + } + + return v +} + +// NewGetOneProjectServiceUnavailable builds a project-service service +// get-one-project endpoint ServiceUnavailable error. +func NewGetOneProjectServiceUnavailable(body *GetOneProjectServiceUnavailableResponseBody) *projectservice.ServiceUnavailableError { + v := &projectservice.ServiceUnavailableError{ + Code: *body.Code, + Message: *body.Message, + } + + return v +} + +// NewUpdateProjectProjectOK builds a "project-service" service +// "update-project" endpoint result from a HTTP "OK" response. +func NewUpdateProjectProjectOK(body *UpdateProjectResponseBody) *projectservice.Project { + v := &projectservice.Project{ + ID: body.ID, + Slug: body.Slug, + Description: body.Description, + Name: body.Name, + Public: body.Public, + ParentUID: body.ParentUID, + } + if body.Auditors != nil { + v.Auditors = make([]string, len(body.Auditors)) + for i, val := range body.Auditors { + v.Auditors[i] = val + } + } + if body.Writers != nil { + v.Writers = make([]string, len(body.Writers)) + for i, val := range body.Writers { + v.Writers[i] = val + } + } + + return v +} + +// NewUpdateProjectBadRequest builds a project-service service update-project +// endpoint BadRequest error. +func NewUpdateProjectBadRequest(body *UpdateProjectBadRequestResponseBody) *projectservice.BadRequestError { + v := &projectservice.BadRequestError{ + Code: *body.Code, + Message: *body.Message, + } + + return v +} + +// NewUpdateProjectInternalServerError builds a project-service service +// update-project endpoint InternalServerError error. +func NewUpdateProjectInternalServerError(body *UpdateProjectInternalServerErrorResponseBody) *projectservice.InternalServerError { + v := &projectservice.InternalServerError{ + Code: *body.Code, + Message: *body.Message, + } + + return v +} + +// NewUpdateProjectNotFound builds a project-service service update-project +// endpoint NotFound error. +func NewUpdateProjectNotFound(body *UpdateProjectNotFoundResponseBody) *projectservice.NotFoundError { + v := &projectservice.NotFoundError{ + Code: *body.Code, + Message: *body.Message, + } + + return v +} + +// NewUpdateProjectServiceUnavailable builds a project-service service +// update-project endpoint ServiceUnavailable error. +func NewUpdateProjectServiceUnavailable(body *UpdateProjectServiceUnavailableResponseBody) *projectservice.ServiceUnavailableError { + v := &projectservice.ServiceUnavailableError{ + Code: *body.Code, + Message: *body.Message, + } + + return v +} + +// NewDeleteProjectBadRequest builds a project-service service delete-project +// endpoint BadRequest error. +func NewDeleteProjectBadRequest(body *DeleteProjectBadRequestResponseBody) *projectservice.BadRequestError { + v := &projectservice.BadRequestError{ + Code: *body.Code, + Message: *body.Message, + } + + return v +} + +// NewDeleteProjectInternalServerError builds a project-service service +// delete-project endpoint InternalServerError error. +func NewDeleteProjectInternalServerError(body *DeleteProjectInternalServerErrorResponseBody) *projectservice.InternalServerError { + v := &projectservice.InternalServerError{ + Code: *body.Code, + Message: *body.Message, + } + + return v +} + +// NewDeleteProjectNotFound builds a project-service service delete-project +// endpoint NotFound error. +func NewDeleteProjectNotFound(body *DeleteProjectNotFoundResponseBody) *projectservice.NotFoundError { + v := &projectservice.NotFoundError{ + Code: *body.Code, + Message: *body.Message, + } + + return v +} + +// NewDeleteProjectServiceUnavailable builds a project-service service +// delete-project endpoint ServiceUnavailable error. +func NewDeleteProjectServiceUnavailable(body *DeleteProjectServiceUnavailableResponseBody) *projectservice.ServiceUnavailableError { + v := &projectservice.ServiceUnavailableError{ + Code: *body.Code, + Message: *body.Message, + } + + return v +} + +// NewReadyzServiceUnavailable builds a project-service service readyz endpoint +// ServiceUnavailable error. +func NewReadyzServiceUnavailable(body *ReadyzServiceUnavailableResponseBody) *projectservice.ServiceUnavailableError { + v := &projectservice.ServiceUnavailableError{ + Code: *body.Code, + Message: *body.Message, + } + + return v +} + +// ValidateGetProjectsResponseBody runs the validations defined on +// Get-ProjectsResponseBody +func ValidateGetProjectsResponseBody(body *GetProjectsResponseBody) (err error) { + if body.Projects == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("projects", "body")) + } + for _, e := range body.Projects { + if e != nil { + if err2 := ValidateProjectResponseBody(e); err2 != nil { + err = goa.MergeErrors(err, err2) + } + } + } + return +} + +// ValidateCreateProjectResponseBody runs the validations defined on +// Create-ProjectResponseBody +func ValidateCreateProjectResponseBody(body *CreateProjectResponseBody) (err error) { + if body.ID != nil { + err = goa.MergeErrors(err, goa.ValidateFormat("body.id", *body.ID, goa.FormatUUID)) + } + if body.Slug != nil { + err = goa.MergeErrors(err, goa.ValidateFormat("body.slug", *body.Slug, goa.FormatRegexp)) + } + if body.Slug != nil { + err = goa.MergeErrors(err, goa.ValidatePattern("body.slug", *body.Slug, "^[a-z][a-z0-9_\\-]*[a-z0-9]$")) + } + return +} + +// ValidateGetOneProjectResponseBody runs the validations defined on +// Get-One-ProjectResponseBody +func ValidateGetOneProjectResponseBody(body *GetOneProjectResponseBody) (err error) { + if body.ID != nil { + err = goa.MergeErrors(err, goa.ValidateFormat("body.id", *body.ID, goa.FormatUUID)) + } + if body.Slug != nil { + err = goa.MergeErrors(err, goa.ValidateFormat("body.slug", *body.Slug, goa.FormatRegexp)) + } + if body.Slug != nil { + err = goa.MergeErrors(err, goa.ValidatePattern("body.slug", *body.Slug, "^[a-z][a-z0-9_\\-]*[a-z0-9]$")) + } + return +} + +// ValidateUpdateProjectResponseBody runs the validations defined on +// Update-ProjectResponseBody +func ValidateUpdateProjectResponseBody(body *UpdateProjectResponseBody) (err error) { + if body.ID != nil { + err = goa.MergeErrors(err, goa.ValidateFormat("body.id", *body.ID, goa.FormatUUID)) + } + if body.Slug != nil { + err = goa.MergeErrors(err, goa.ValidateFormat("body.slug", *body.Slug, goa.FormatRegexp)) + } + if body.Slug != nil { + err = goa.MergeErrors(err, goa.ValidatePattern("body.slug", *body.Slug, "^[a-z][a-z0-9_\\-]*[a-z0-9]$")) + } + return +} + +// ValidateGetProjectsBadRequestResponseBody runs the validations defined on +// get-projects_BadRequest_response_body +func ValidateGetProjectsBadRequestResponseBody(body *GetProjectsBadRequestResponseBody) (err error) { + if body.Code == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("code", "body")) + } + if body.Message == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("message", "body")) + } + return +} + +// ValidateGetProjectsInternalServerErrorResponseBody runs the validations +// defined on get-projects_InternalServerError_response_body +func ValidateGetProjectsInternalServerErrorResponseBody(body *GetProjectsInternalServerErrorResponseBody) (err error) { + if body.Code == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("code", "body")) + } + if body.Message == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("message", "body")) + } + return +} + +// ValidateGetProjectsServiceUnavailableResponseBody runs the validations +// defined on get-projects_ServiceUnavailable_response_body +func ValidateGetProjectsServiceUnavailableResponseBody(body *GetProjectsServiceUnavailableResponseBody) (err error) { + if body.Code == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("code", "body")) + } + if body.Message == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("message", "body")) + } + return +} + +// ValidateCreateProjectBadRequestResponseBody runs the validations defined on +// create-project_BadRequest_response_body +func ValidateCreateProjectBadRequestResponseBody(body *CreateProjectBadRequestResponseBody) (err error) { + if body.Code == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("code", "body")) + } + if body.Message == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("message", "body")) + } + return +} + +// ValidateCreateProjectConflictResponseBody runs the validations defined on +// create-project_Conflict_response_body +func ValidateCreateProjectConflictResponseBody(body *CreateProjectConflictResponseBody) (err error) { + if body.Code == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("code", "body")) + } + if body.Message == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("message", "body")) + } + return +} + +// ValidateCreateProjectInternalServerErrorResponseBody runs the validations +// defined on create-project_InternalServerError_response_body +func ValidateCreateProjectInternalServerErrorResponseBody(body *CreateProjectInternalServerErrorResponseBody) (err error) { + if body.Code == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("code", "body")) + } + if body.Message == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("message", "body")) + } + return +} + +// ValidateCreateProjectServiceUnavailableResponseBody runs the validations +// defined on create-project_ServiceUnavailable_response_body +func ValidateCreateProjectServiceUnavailableResponseBody(body *CreateProjectServiceUnavailableResponseBody) (err error) { + if body.Code == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("code", "body")) + } + if body.Message == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("message", "body")) + } + return +} + +// ValidateGetOneProjectInternalServerErrorResponseBody runs the validations +// defined on get-one-project_InternalServerError_response_body +func ValidateGetOneProjectInternalServerErrorResponseBody(body *GetOneProjectInternalServerErrorResponseBody) (err error) { + if body.Code == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("code", "body")) + } + if body.Message == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("message", "body")) + } + return +} + +// ValidateGetOneProjectNotFoundResponseBody runs the validations defined on +// get-one-project_NotFound_response_body +func ValidateGetOneProjectNotFoundResponseBody(body *GetOneProjectNotFoundResponseBody) (err error) { + if body.Code == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("code", "body")) + } + if body.Message == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("message", "body")) + } + return +} + +// ValidateGetOneProjectServiceUnavailableResponseBody runs the validations +// defined on get-one-project_ServiceUnavailable_response_body +func ValidateGetOneProjectServiceUnavailableResponseBody(body *GetOneProjectServiceUnavailableResponseBody) (err error) { + if body.Code == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("code", "body")) + } + if body.Message == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("message", "body")) + } + return +} + +// ValidateUpdateProjectBadRequestResponseBody runs the validations defined on +// update-project_BadRequest_response_body +func ValidateUpdateProjectBadRequestResponseBody(body *UpdateProjectBadRequestResponseBody) (err error) { + if body.Code == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("code", "body")) + } + if body.Message == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("message", "body")) + } + return +} + +// ValidateUpdateProjectInternalServerErrorResponseBody runs the validations +// defined on update-project_InternalServerError_response_body +func ValidateUpdateProjectInternalServerErrorResponseBody(body *UpdateProjectInternalServerErrorResponseBody) (err error) { + if body.Code == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("code", "body")) + } + if body.Message == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("message", "body")) + } + return +} + +// ValidateUpdateProjectNotFoundResponseBody runs the validations defined on +// update-project_NotFound_response_body +func ValidateUpdateProjectNotFoundResponseBody(body *UpdateProjectNotFoundResponseBody) (err error) { + if body.Code == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("code", "body")) + } + if body.Message == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("message", "body")) + } + return +} + +// ValidateUpdateProjectServiceUnavailableResponseBody runs the validations +// defined on update-project_ServiceUnavailable_response_body +func ValidateUpdateProjectServiceUnavailableResponseBody(body *UpdateProjectServiceUnavailableResponseBody) (err error) { + if body.Code == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("code", "body")) + } + if body.Message == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("message", "body")) + } + return +} + +// ValidateDeleteProjectBadRequestResponseBody runs the validations defined on +// delete-project_BadRequest_response_body +func ValidateDeleteProjectBadRequestResponseBody(body *DeleteProjectBadRequestResponseBody) (err error) { + if body.Code == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("code", "body")) + } + if body.Message == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("message", "body")) + } + return +} + +// ValidateDeleteProjectInternalServerErrorResponseBody runs the validations +// defined on delete-project_InternalServerError_response_body +func ValidateDeleteProjectInternalServerErrorResponseBody(body *DeleteProjectInternalServerErrorResponseBody) (err error) { + if body.Code == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("code", "body")) + } + if body.Message == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("message", "body")) + } + return +} + +// ValidateDeleteProjectNotFoundResponseBody runs the validations defined on +// delete-project_NotFound_response_body +func ValidateDeleteProjectNotFoundResponseBody(body *DeleteProjectNotFoundResponseBody) (err error) { + if body.Code == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("code", "body")) + } + if body.Message == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("message", "body")) + } + return +} + +// ValidateDeleteProjectServiceUnavailableResponseBody runs the validations +// defined on delete-project_ServiceUnavailable_response_body +func ValidateDeleteProjectServiceUnavailableResponseBody(body *DeleteProjectServiceUnavailableResponseBody) (err error) { + if body.Code == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("code", "body")) + } + if body.Message == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("message", "body")) + } + return +} + +// ValidateReadyzServiceUnavailableResponseBody runs the validations defined on +// readyz_ServiceUnavailable_response_body +func ValidateReadyzServiceUnavailableResponseBody(body *ReadyzServiceUnavailableResponseBody) (err error) { + if body.Code == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("code", "body")) + } + if body.Message == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("message", "body")) + } + return +} + +// ValidateProjectResponseBody runs the validations defined on +// ProjectResponseBody +func ValidateProjectResponseBody(body *ProjectResponseBody) (err error) { + if body.ID != nil { + err = goa.MergeErrors(err, goa.ValidateFormat("body.id", *body.ID, goa.FormatUUID)) + } + if body.Slug != nil { + err = goa.MergeErrors(err, goa.ValidateFormat("body.slug", *body.Slug, goa.FormatRegexp)) + } + if body.Slug != nil { + err = goa.MergeErrors(err, goa.ValidatePattern("body.slug", *body.Slug, "^[a-z][a-z0-9_\\-]*[a-z0-9]$")) + } + return +} diff --git a/cmd/project-api/gen/http/project_service/server/encode_decode.go b/cmd/project-api/gen/http/project_service/server/encode_decode.go new file mode 100644 index 0000000..2ddbf74 --- /dev/null +++ b/cmd/project-api/gen/http/project_service/server/encode_decode.go @@ -0,0 +1,733 @@ +// Code generated by goa v3.21.1, DO NOT EDIT. +// +// project-service HTTP server encoders and decoders +// +// Command: +// $ goa gen +// github.com/linuxfoundation/lfx-v2-project-service/cmd/project-api/design + +package server + +import ( + "context" + "errors" + "io" + "net/http" + "strings" + + projectservice "github.com/linuxfoundation/lfx-v2-project-service/cmd/project-api/gen/project_service" + goahttp "goa.design/goa/v3/http" + goa "goa.design/goa/v3/pkg" +) + +// EncodeGetProjectsResponse returns an encoder for responses returned by the +// project-service get-projects endpoint. +func EncodeGetProjectsResponse(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder) func(context.Context, http.ResponseWriter, any) error { + return func(ctx context.Context, w http.ResponseWriter, v any) error { + res, _ := v.(*projectservice.GetProjectsResult) + enc := encoder(ctx, w) + body := NewGetProjectsResponseBody(res) + if res.CacheControl != nil { + w.Header().Set("Cache-Control", *res.CacheControl) + } + w.WriteHeader(http.StatusOK) + return enc.Encode(body) + } +} + +// DecodeGetProjectsRequest returns a decoder for requests sent to the +// project-service get-projects endpoint. +func DecodeGetProjectsRequest(mux goahttp.Muxer, decoder func(*http.Request) goahttp.Decoder) func(*http.Request) (any, error) { + return func(r *http.Request) (any, error) { + var ( + version *string + bearerToken *string + err error + ) + versionRaw := r.URL.Query().Get("v") + if versionRaw != "" { + version = &versionRaw + } + if version != nil { + if !(*version == "1") { + err = goa.MergeErrors(err, goa.InvalidEnumValueError("version", *version, []any{"1"})) + } + } + bearerTokenRaw := r.Header.Get("Authorization") + if bearerTokenRaw != "" { + bearerToken = &bearerTokenRaw + } + if err != nil { + return nil, err + } + payload := NewGetProjectsPayload(version, bearerToken) + if payload.BearerToken != nil { + if strings.Contains(*payload.BearerToken, " ") { + // Remove authorization scheme prefix (e.g. "Bearer") + cred := strings.SplitN(*payload.BearerToken, " ", 2)[1] + payload.BearerToken = &cred + } + } + + return payload, nil + } +} + +// EncodeGetProjectsError returns an encoder for errors returned by the +// get-projects project-service endpoint. +func EncodeGetProjectsError(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, formatter func(ctx context.Context, err error) goahttp.Statuser) func(context.Context, http.ResponseWriter, error) error { + encodeError := goahttp.ErrorEncoder(encoder, formatter) + return func(ctx context.Context, w http.ResponseWriter, v error) error { + var en goa.GoaErrorNamer + if !errors.As(v, &en) { + return encodeError(ctx, w, v) + } + switch en.GoaErrorName() { + case "BadRequest": + var res *projectservice.BadRequestError + errors.As(v, &res) + enc := encoder(ctx, w) + var body any + if formatter != nil { + body = formatter(ctx, res) + } else { + body = NewGetProjectsBadRequestResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusBadRequest) + return enc.Encode(body) + case "InternalServerError": + var res *projectservice.InternalServerError + errors.As(v, &res) + enc := encoder(ctx, w) + var body any + if formatter != nil { + body = formatter(ctx, res) + } else { + body = NewGetProjectsInternalServerErrorResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusInternalServerError) + return enc.Encode(body) + case "ServiceUnavailable": + var res *projectservice.ServiceUnavailableError + errors.As(v, &res) + enc := encoder(ctx, w) + var body any + if formatter != nil { + body = formatter(ctx, res) + } else { + body = NewGetProjectsServiceUnavailableResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusServiceUnavailable) + return enc.Encode(body) + default: + return encodeError(ctx, w, v) + } + } +} + +// EncodeCreateProjectResponse returns an encoder for responses returned by the +// project-service create-project endpoint. +func EncodeCreateProjectResponse(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder) func(context.Context, http.ResponseWriter, any) error { + return func(ctx context.Context, w http.ResponseWriter, v any) error { + res, _ := v.(*projectservice.Project) + enc := encoder(ctx, w) + body := NewCreateProjectResponseBody(res) + w.WriteHeader(http.StatusCreated) + return enc.Encode(body) + } +} + +// DecodeCreateProjectRequest returns a decoder for requests sent to the +// project-service create-project endpoint. +func DecodeCreateProjectRequest(mux goahttp.Muxer, decoder func(*http.Request) goahttp.Decoder) func(*http.Request) (any, error) { + return func(r *http.Request) (any, error) { + var ( + body CreateProjectRequestBody + err error + ) + err = decoder(r).Decode(&body) + if err != nil { + if err == io.EOF { + return nil, goa.MissingPayloadError() + } + var gerr *goa.ServiceError + if errors.As(err, &gerr) { + return nil, gerr + } + return nil, goa.DecodePayloadError(err.Error()) + } + err = ValidateCreateProjectRequestBody(&body) + if err != nil { + return nil, err + } + + var ( + version *string + bearerToken *string + ) + versionRaw := r.URL.Query().Get("v") + if versionRaw != "" { + version = &versionRaw + } + if version != nil { + if !(*version == "1") { + err = goa.MergeErrors(err, goa.InvalidEnumValueError("version", *version, []any{"1"})) + } + } + bearerTokenRaw := r.Header.Get("Authorization") + if bearerTokenRaw != "" { + bearerToken = &bearerTokenRaw + } + if err != nil { + return nil, err + } + payload := NewCreateProjectPayload(&body, version, bearerToken) + if payload.BearerToken != nil { + if strings.Contains(*payload.BearerToken, " ") { + // Remove authorization scheme prefix (e.g. "Bearer") + cred := strings.SplitN(*payload.BearerToken, " ", 2)[1] + payload.BearerToken = &cred + } + } + + return payload, nil + } +} + +// EncodeCreateProjectError returns an encoder for errors returned by the +// create-project project-service endpoint. +func EncodeCreateProjectError(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, formatter func(ctx context.Context, err error) goahttp.Statuser) func(context.Context, http.ResponseWriter, error) error { + encodeError := goahttp.ErrorEncoder(encoder, formatter) + return func(ctx context.Context, w http.ResponseWriter, v error) error { + var en goa.GoaErrorNamer + if !errors.As(v, &en) { + return encodeError(ctx, w, v) + } + switch en.GoaErrorName() { + case "BadRequest": + var res *projectservice.BadRequestError + errors.As(v, &res) + enc := encoder(ctx, w) + var body any + if formatter != nil { + body = formatter(ctx, res) + } else { + body = NewCreateProjectBadRequestResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusBadRequest) + return enc.Encode(body) + case "Conflict": + var res *projectservice.ConflictError + errors.As(v, &res) + enc := encoder(ctx, w) + var body any + if formatter != nil { + body = formatter(ctx, res) + } else { + body = NewCreateProjectConflictResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusConflict) + return enc.Encode(body) + case "InternalServerError": + var res *projectservice.InternalServerError + errors.As(v, &res) + enc := encoder(ctx, w) + var body any + if formatter != nil { + body = formatter(ctx, res) + } else { + body = NewCreateProjectInternalServerErrorResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusInternalServerError) + return enc.Encode(body) + case "ServiceUnavailable": + var res *projectservice.ServiceUnavailableError + errors.As(v, &res) + enc := encoder(ctx, w) + var body any + if formatter != nil { + body = formatter(ctx, res) + } else { + body = NewCreateProjectServiceUnavailableResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusServiceUnavailable) + return enc.Encode(body) + default: + return encodeError(ctx, w, v) + } + } +} + +// EncodeGetOneProjectResponse returns an encoder for responses returned by the +// project-service get-one-project endpoint. +func EncodeGetOneProjectResponse(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder) func(context.Context, http.ResponseWriter, any) error { + return func(ctx context.Context, w http.ResponseWriter, v any) error { + res, _ := v.(*projectservice.GetOneProjectResult) + enc := encoder(ctx, w) + body := NewGetOneProjectResponseBody(res) + if res.Etag != nil { + w.Header().Set("Etag", *res.Etag) + } + w.WriteHeader(http.StatusOK) + return enc.Encode(body) + } +} + +// DecodeGetOneProjectRequest returns a decoder for requests sent to the +// project-service get-one-project endpoint. +func DecodeGetOneProjectRequest(mux goahttp.Muxer, decoder func(*http.Request) goahttp.Decoder) func(*http.Request) (any, error) { + return func(r *http.Request) (any, error) { + var ( + id string + version *string + bearerToken *string + err error + + params = mux.Vars(r) + ) + id = params["id"] + err = goa.MergeErrors(err, goa.ValidateFormat("id", id, goa.FormatUUID)) + versionRaw := r.URL.Query().Get("v") + if versionRaw != "" { + version = &versionRaw + } + if version != nil { + if !(*version == "1") { + err = goa.MergeErrors(err, goa.InvalidEnumValueError("version", *version, []any{"1"})) + } + } + bearerTokenRaw := r.Header.Get("Authorization") + if bearerTokenRaw != "" { + bearerToken = &bearerTokenRaw + } + if err != nil { + return nil, err + } + payload := NewGetOneProjectPayload(id, version, bearerToken) + if payload.BearerToken != nil { + if strings.Contains(*payload.BearerToken, " ") { + // Remove authorization scheme prefix (e.g. "Bearer") + cred := strings.SplitN(*payload.BearerToken, " ", 2)[1] + payload.BearerToken = &cred + } + } + + return payload, nil + } +} + +// EncodeGetOneProjectError returns an encoder for errors returned by the +// get-one-project project-service endpoint. +func EncodeGetOneProjectError(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, formatter func(ctx context.Context, err error) goahttp.Statuser) func(context.Context, http.ResponseWriter, error) error { + encodeError := goahttp.ErrorEncoder(encoder, formatter) + return func(ctx context.Context, w http.ResponseWriter, v error) error { + var en goa.GoaErrorNamer + if !errors.As(v, &en) { + return encodeError(ctx, w, v) + } + switch en.GoaErrorName() { + case "InternalServerError": + var res *projectservice.InternalServerError + errors.As(v, &res) + enc := encoder(ctx, w) + var body any + if formatter != nil { + body = formatter(ctx, res) + } else { + body = NewGetOneProjectInternalServerErrorResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusInternalServerError) + return enc.Encode(body) + case "NotFound": + var res *projectservice.NotFoundError + errors.As(v, &res) + enc := encoder(ctx, w) + var body any + if formatter != nil { + body = formatter(ctx, res) + } else { + body = NewGetOneProjectNotFoundResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusNotFound) + return enc.Encode(body) + case "ServiceUnavailable": + var res *projectservice.ServiceUnavailableError + errors.As(v, &res) + enc := encoder(ctx, w) + var body any + if formatter != nil { + body = formatter(ctx, res) + } else { + body = NewGetOneProjectServiceUnavailableResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusServiceUnavailable) + return enc.Encode(body) + default: + return encodeError(ctx, w, v) + } + } +} + +// EncodeUpdateProjectResponse returns an encoder for responses returned by the +// project-service update-project endpoint. +func EncodeUpdateProjectResponse(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder) func(context.Context, http.ResponseWriter, any) error { + return func(ctx context.Context, w http.ResponseWriter, v any) error { + res, _ := v.(*projectservice.Project) + enc := encoder(ctx, w) + body := NewUpdateProjectResponseBody(res) + w.WriteHeader(http.StatusOK) + return enc.Encode(body) + } +} + +// DecodeUpdateProjectRequest returns a decoder for requests sent to the +// project-service update-project endpoint. +func DecodeUpdateProjectRequest(mux goahttp.Muxer, decoder func(*http.Request) goahttp.Decoder) func(*http.Request) (any, error) { + return func(r *http.Request) (any, error) { + var ( + body UpdateProjectRequestBody + err error + ) + err = decoder(r).Decode(&body) + if err != nil { + if err == io.EOF { + return nil, goa.MissingPayloadError() + } + var gerr *goa.ServiceError + if errors.As(err, &gerr) { + return nil, gerr + } + return nil, goa.DecodePayloadError(err.Error()) + } + err = ValidateUpdateProjectRequestBody(&body) + if err != nil { + return nil, err + } + + var ( + id string + version *string + bearerToken *string + etag *string + + params = mux.Vars(r) + ) + id = params["id"] + err = goa.MergeErrors(err, goa.ValidateFormat("id", id, goa.FormatUUID)) + versionRaw := r.URL.Query().Get("v") + if versionRaw != "" { + version = &versionRaw + } + if version != nil { + if !(*version == "1") { + err = goa.MergeErrors(err, goa.InvalidEnumValueError("version", *version, []any{"1"})) + } + } + bearerTokenRaw := r.Header.Get("Authorization") + if bearerTokenRaw != "" { + bearerToken = &bearerTokenRaw + } + etagRaw := r.Header.Get("ETag") + if etagRaw != "" { + etag = &etagRaw + } + if err != nil { + return nil, err + } + payload := NewUpdateProjectPayload(&body, id, version, bearerToken, etag) + if payload.BearerToken != nil { + if strings.Contains(*payload.BearerToken, " ") { + // Remove authorization scheme prefix (e.g. "Bearer") + cred := strings.SplitN(*payload.BearerToken, " ", 2)[1] + payload.BearerToken = &cred + } + } + + return payload, nil + } +} + +// EncodeUpdateProjectError returns an encoder for errors returned by the +// update-project project-service endpoint. +func EncodeUpdateProjectError(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, formatter func(ctx context.Context, err error) goahttp.Statuser) func(context.Context, http.ResponseWriter, error) error { + encodeError := goahttp.ErrorEncoder(encoder, formatter) + return func(ctx context.Context, w http.ResponseWriter, v error) error { + var en goa.GoaErrorNamer + if !errors.As(v, &en) { + return encodeError(ctx, w, v) + } + switch en.GoaErrorName() { + case "BadRequest": + var res *projectservice.BadRequestError + errors.As(v, &res) + enc := encoder(ctx, w) + var body any + if formatter != nil { + body = formatter(ctx, res) + } else { + body = NewUpdateProjectBadRequestResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusBadRequest) + return enc.Encode(body) + case "InternalServerError": + var res *projectservice.InternalServerError + errors.As(v, &res) + enc := encoder(ctx, w) + var body any + if formatter != nil { + body = formatter(ctx, res) + } else { + body = NewUpdateProjectInternalServerErrorResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusInternalServerError) + return enc.Encode(body) + case "NotFound": + var res *projectservice.NotFoundError + errors.As(v, &res) + enc := encoder(ctx, w) + var body any + if formatter != nil { + body = formatter(ctx, res) + } else { + body = NewUpdateProjectNotFoundResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusNotFound) + return enc.Encode(body) + case "ServiceUnavailable": + var res *projectservice.ServiceUnavailableError + errors.As(v, &res) + enc := encoder(ctx, w) + var body any + if formatter != nil { + body = formatter(ctx, res) + } else { + body = NewUpdateProjectServiceUnavailableResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusServiceUnavailable) + return enc.Encode(body) + default: + return encodeError(ctx, w, v) + } + } +} + +// EncodeDeleteProjectResponse returns an encoder for responses returned by the +// project-service delete-project endpoint. +func EncodeDeleteProjectResponse(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder) func(context.Context, http.ResponseWriter, any) error { + return func(ctx context.Context, w http.ResponseWriter, v any) error { + w.WriteHeader(http.StatusNoContent) + return nil + } +} + +// DecodeDeleteProjectRequest returns a decoder for requests sent to the +// project-service delete-project endpoint. +func DecodeDeleteProjectRequest(mux goahttp.Muxer, decoder func(*http.Request) goahttp.Decoder) func(*http.Request) (any, error) { + return func(r *http.Request) (any, error) { + var ( + id string + version *string + bearerToken *string + etag *string + err error + + params = mux.Vars(r) + ) + id = params["id"] + err = goa.MergeErrors(err, goa.ValidateFormat("id", id, goa.FormatUUID)) + versionRaw := r.URL.Query().Get("v") + if versionRaw != "" { + version = &versionRaw + } + if version != nil { + if !(*version == "1") { + err = goa.MergeErrors(err, goa.InvalidEnumValueError("version", *version, []any{"1"})) + } + } + bearerTokenRaw := r.Header.Get("Authorization") + if bearerTokenRaw != "" { + bearerToken = &bearerTokenRaw + } + etagRaw := r.Header.Get("ETag") + if etagRaw != "" { + etag = &etagRaw + } + if err != nil { + return nil, err + } + payload := NewDeleteProjectPayload(id, version, bearerToken, etag) + if payload.BearerToken != nil { + if strings.Contains(*payload.BearerToken, " ") { + // Remove authorization scheme prefix (e.g. "Bearer") + cred := strings.SplitN(*payload.BearerToken, " ", 2)[1] + payload.BearerToken = &cred + } + } + + return payload, nil + } +} + +// EncodeDeleteProjectError returns an encoder for errors returned by the +// delete-project project-service endpoint. +func EncodeDeleteProjectError(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, formatter func(ctx context.Context, err error) goahttp.Statuser) func(context.Context, http.ResponseWriter, error) error { + encodeError := goahttp.ErrorEncoder(encoder, formatter) + return func(ctx context.Context, w http.ResponseWriter, v error) error { + var en goa.GoaErrorNamer + if !errors.As(v, &en) { + return encodeError(ctx, w, v) + } + switch en.GoaErrorName() { + case "BadRequest": + var res *projectservice.BadRequestError + errors.As(v, &res) + enc := encoder(ctx, w) + var body any + if formatter != nil { + body = formatter(ctx, res) + } else { + body = NewDeleteProjectBadRequestResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusBadRequest) + return enc.Encode(body) + case "InternalServerError": + var res *projectservice.InternalServerError + errors.As(v, &res) + enc := encoder(ctx, w) + var body any + if formatter != nil { + body = formatter(ctx, res) + } else { + body = NewDeleteProjectInternalServerErrorResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusInternalServerError) + return enc.Encode(body) + case "NotFound": + var res *projectservice.NotFoundError + errors.As(v, &res) + enc := encoder(ctx, w) + var body any + if formatter != nil { + body = formatter(ctx, res) + } else { + body = NewDeleteProjectNotFoundResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusNotFound) + return enc.Encode(body) + case "ServiceUnavailable": + var res *projectservice.ServiceUnavailableError + errors.As(v, &res) + enc := encoder(ctx, w) + var body any + if formatter != nil { + body = formatter(ctx, res) + } else { + body = NewDeleteProjectServiceUnavailableResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusServiceUnavailable) + return enc.Encode(body) + default: + return encodeError(ctx, w, v) + } + } +} + +// EncodeReadyzResponse returns an encoder for responses returned by the +// project-service readyz endpoint. +func EncodeReadyzResponse(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder) func(context.Context, http.ResponseWriter, any) error { + return func(ctx context.Context, w http.ResponseWriter, v any) error { + res, _ := v.([]byte) + ctx = context.WithValue(ctx, goahttp.ContentTypeKey, "text/plain") + enc := encoder(ctx, w) + body := res + w.WriteHeader(http.StatusOK) + return enc.Encode(body) + } +} + +// EncodeReadyzError returns an encoder for errors returned by the readyz +// project-service endpoint. +func EncodeReadyzError(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, formatter func(ctx context.Context, err error) goahttp.Statuser) func(context.Context, http.ResponseWriter, error) error { + encodeError := goahttp.ErrorEncoder(encoder, formatter) + return func(ctx context.Context, w http.ResponseWriter, v error) error { + var en goa.GoaErrorNamer + if !errors.As(v, &en) { + return encodeError(ctx, w, v) + } + switch en.GoaErrorName() { + case "ServiceUnavailable": + var res *projectservice.ServiceUnavailableError + errors.As(v, &res) + enc := encoder(ctx, w) + var body any + if formatter != nil { + body = formatter(ctx, res) + } else { + body = NewReadyzServiceUnavailableResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusServiceUnavailable) + return enc.Encode(body) + default: + return encodeError(ctx, w, v) + } + } +} + +// EncodeLivezResponse returns an encoder for responses returned by the +// project-service livez endpoint. +func EncodeLivezResponse(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder) func(context.Context, http.ResponseWriter, any) error { + return func(ctx context.Context, w http.ResponseWriter, v any) error { + res, _ := v.([]byte) + ctx = context.WithValue(ctx, goahttp.ContentTypeKey, "text/plain") + enc := encoder(ctx, w) + body := res + w.WriteHeader(http.StatusOK) + return enc.Encode(body) + } +} + +// marshalProjectserviceProjectToProjectResponseBody builds a value of type +// *ProjectResponseBody from a value of type *projectservice.Project. +func marshalProjectserviceProjectToProjectResponseBody(v *projectservice.Project) *ProjectResponseBody { + res := &ProjectResponseBody{ + ID: v.ID, + Slug: v.Slug, + Description: v.Description, + Name: v.Name, + Public: v.Public, + ParentUID: v.ParentUID, + } + if v.Auditors != nil { + res.Auditors = make([]string, len(v.Auditors)) + for i, val := range v.Auditors { + res.Auditors[i] = val + } + } + if v.Writers != nil { + res.Writers = make([]string, len(v.Writers)) + for i, val := range v.Writers { + res.Writers[i] = val + } + } + + return res +} diff --git a/cmd/project-api/gen/http/project_service/server/paths.go b/cmd/project-api/gen/http/project_service/server/paths.go new file mode 100644 index 0000000..ae1e570 --- /dev/null +++ b/cmd/project-api/gen/http/project_service/server/paths.go @@ -0,0 +1,48 @@ +// Code generated by goa v3.21.1, DO NOT EDIT. +// +// HTTP request path constructors for the project-service service. +// +// Command: +// $ goa gen +// github.com/linuxfoundation/lfx-v2-project-service/cmd/project-api/design + +package server + +import ( + "fmt" +) + +// GetProjectsProjectServicePath returns the URL path to the project-service service get-projects HTTP endpoint. +func GetProjectsProjectServicePath() string { + return "/projects" +} + +// CreateProjectProjectServicePath returns the URL path to the project-service service create-project HTTP endpoint. +func CreateProjectProjectServicePath() string { + return "/projects" +} + +// GetOneProjectProjectServicePath returns the URL path to the project-service service get-one-project HTTP endpoint. +func GetOneProjectProjectServicePath(id string) string { + return fmt.Sprintf("/projects/%v", id) +} + +// UpdateProjectProjectServicePath returns the URL path to the project-service service update-project HTTP endpoint. +func UpdateProjectProjectServicePath(id string) string { + return fmt.Sprintf("/projects/%v", id) +} + +// DeleteProjectProjectServicePath returns the URL path to the project-service service delete-project HTTP endpoint. +func DeleteProjectProjectServicePath(id string) string { + return fmt.Sprintf("/projects/%v", id) +} + +// ReadyzProjectServicePath returns the URL path to the project-service service readyz HTTP endpoint. +func ReadyzProjectServicePath() string { + return "/readyz" +} + +// LivezProjectServicePath returns the URL path to the project-service service livez HTTP endpoint. +func LivezProjectServicePath() string { + return "/livez" +} diff --git a/cmd/project-api/gen/http/project_service/server/server.go b/cmd/project-api/gen/http/project_service/server/server.go new file mode 100644 index 0000000..925604d --- /dev/null +++ b/cmd/project-api/gen/http/project_service/server/server.go @@ -0,0 +1,490 @@ +// Code generated by goa v3.21.1, DO NOT EDIT. +// +// project-service HTTP server +// +// Command: +// $ goa gen +// github.com/linuxfoundation/lfx-v2-project-service/cmd/project-api/design + +package server + +import ( + "context" + "net/http" + "path" + + projectservice "github.com/linuxfoundation/lfx-v2-project-service/cmd/project-api/gen/project_service" + goahttp "goa.design/goa/v3/http" + goa "goa.design/goa/v3/pkg" +) + +// Server lists the project-service service endpoint HTTP handlers. +type Server struct { + Mounts []*MountPoint + GetProjects http.Handler + CreateProject http.Handler + GetOneProject http.Handler + UpdateProject http.Handler + DeleteProject http.Handler + Readyz http.Handler + Livez http.Handler + GenHTTPOpenapi3JSON http.Handler +} + +// MountPoint holds information about the mounted endpoints. +type MountPoint struct { + // Method is the name of the service method served by the mounted HTTP handler. + Method string + // Verb is the HTTP method used to match requests to the mounted handler. + Verb string + // Pattern is the HTTP request path pattern used to match requests to the + // mounted handler. + Pattern string +} + +// New instantiates HTTP handlers for all the project-service service endpoints +// using the provided encoder and decoder. The handlers are mounted on the +// given mux using the HTTP verb and path defined in the design. errhandler is +// called whenever a response fails to be encoded. formatter is used to format +// errors returned by the service methods prior to encoding. Both errhandler +// and formatter are optional and can be nil. +func New( + e *projectservice.Endpoints, + mux goahttp.Muxer, + decoder func(*http.Request) goahttp.Decoder, + encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, + errhandler func(context.Context, http.ResponseWriter, error), + formatter func(ctx context.Context, err error) goahttp.Statuser, + fileSystemGenHTTPOpenapi3JSON http.FileSystem, +) *Server { + if fileSystemGenHTTPOpenapi3JSON == nil { + fileSystemGenHTTPOpenapi3JSON = http.Dir(".") + } + fileSystemGenHTTPOpenapi3JSON = appendPrefix(fileSystemGenHTTPOpenapi3JSON, "/gen/http") + return &Server{ + Mounts: []*MountPoint{ + {"GetProjects", "GET", "/projects"}, + {"CreateProject", "POST", "/projects"}, + {"GetOneProject", "GET", "/projects/{id}"}, + {"UpdateProject", "PUT", "/projects/{id}"}, + {"DeleteProject", "DELETE", "/projects/{id}"}, + {"Readyz", "GET", "/readyz"}, + {"Livez", "GET", "/livez"}, + {"Serve gen/http/openapi3.json", "GET", "/openapi.json"}, + }, + GetProjects: NewGetProjectsHandler(e.GetProjects, mux, decoder, encoder, errhandler, formatter), + CreateProject: NewCreateProjectHandler(e.CreateProject, mux, decoder, encoder, errhandler, formatter), + GetOneProject: NewGetOneProjectHandler(e.GetOneProject, mux, decoder, encoder, errhandler, formatter), + UpdateProject: NewUpdateProjectHandler(e.UpdateProject, mux, decoder, encoder, errhandler, formatter), + DeleteProject: NewDeleteProjectHandler(e.DeleteProject, mux, decoder, encoder, errhandler, formatter), + Readyz: NewReadyzHandler(e.Readyz, mux, decoder, encoder, errhandler, formatter), + Livez: NewLivezHandler(e.Livez, mux, decoder, encoder, errhandler, formatter), + GenHTTPOpenapi3JSON: http.FileServer(fileSystemGenHTTPOpenapi3JSON), + } +} + +// Service returns the name of the service served. +func (s *Server) Service() string { return "project-service" } + +// Use wraps the server handlers with the given middleware. +func (s *Server) Use(m func(http.Handler) http.Handler) { + s.GetProjects = m(s.GetProjects) + s.CreateProject = m(s.CreateProject) + s.GetOneProject = m(s.GetOneProject) + s.UpdateProject = m(s.UpdateProject) + s.DeleteProject = m(s.DeleteProject) + s.Readyz = m(s.Readyz) + s.Livez = m(s.Livez) +} + +// MethodNames returns the methods served. +func (s *Server) MethodNames() []string { return projectservice.MethodNames[:] } + +// Mount configures the mux to serve the project-service endpoints. +func Mount(mux goahttp.Muxer, h *Server) { + MountGetProjectsHandler(mux, h.GetProjects) + MountCreateProjectHandler(mux, h.CreateProject) + MountGetOneProjectHandler(mux, h.GetOneProject) + MountUpdateProjectHandler(mux, h.UpdateProject) + MountDeleteProjectHandler(mux, h.DeleteProject) + MountReadyzHandler(mux, h.Readyz) + MountLivezHandler(mux, h.Livez) + MountGenHTTPOpenapi3JSON(mux, h.GenHTTPOpenapi3JSON) +} + +// Mount configures the mux to serve the project-service endpoints. +func (s *Server) Mount(mux goahttp.Muxer) { + Mount(mux, s) +} + +// MountGetProjectsHandler configures the mux to serve the "project-service" +// service "get-projects" endpoint. +func MountGetProjectsHandler(mux goahttp.Muxer, h http.Handler) { + f, ok := h.(http.HandlerFunc) + if !ok { + f = func(w http.ResponseWriter, r *http.Request) { + h.ServeHTTP(w, r) + } + } + mux.Handle("GET", "/projects", f) +} + +// NewGetProjectsHandler creates a HTTP handler which loads the HTTP request +// and calls the "project-service" service "get-projects" endpoint. +func NewGetProjectsHandler( + endpoint goa.Endpoint, + mux goahttp.Muxer, + decoder func(*http.Request) goahttp.Decoder, + encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, + errhandler func(context.Context, http.ResponseWriter, error), + formatter func(ctx context.Context, err error) goahttp.Statuser, +) http.Handler { + var ( + decodeRequest = DecodeGetProjectsRequest(mux, decoder) + encodeResponse = EncodeGetProjectsResponse(encoder) + encodeError = EncodeGetProjectsError(encoder, formatter) + ) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := context.WithValue(r.Context(), goahttp.AcceptTypeKey, r.Header.Get("Accept")) + ctx = context.WithValue(ctx, goa.MethodKey, "get-projects") + ctx = context.WithValue(ctx, goa.ServiceKey, "project-service") + payload, err := decodeRequest(r) + if err != nil { + if err := encodeError(ctx, w, err); err != nil { + errhandler(ctx, w, err) + } + return + } + res, err := endpoint(ctx, payload) + if err != nil { + if err := encodeError(ctx, w, err); err != nil { + errhandler(ctx, w, err) + } + return + } + if err := encodeResponse(ctx, w, res); err != nil { + errhandler(ctx, w, err) + } + }) +} + +// MountCreateProjectHandler configures the mux to serve the "project-service" +// service "create-project" endpoint. +func MountCreateProjectHandler(mux goahttp.Muxer, h http.Handler) { + f, ok := h.(http.HandlerFunc) + if !ok { + f = func(w http.ResponseWriter, r *http.Request) { + h.ServeHTTP(w, r) + } + } + mux.Handle("POST", "/projects", f) +} + +// NewCreateProjectHandler creates a HTTP handler which loads the HTTP request +// and calls the "project-service" service "create-project" endpoint. +func NewCreateProjectHandler( + endpoint goa.Endpoint, + mux goahttp.Muxer, + decoder func(*http.Request) goahttp.Decoder, + encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, + errhandler func(context.Context, http.ResponseWriter, error), + formatter func(ctx context.Context, err error) goahttp.Statuser, +) http.Handler { + var ( + decodeRequest = DecodeCreateProjectRequest(mux, decoder) + encodeResponse = EncodeCreateProjectResponse(encoder) + encodeError = EncodeCreateProjectError(encoder, formatter) + ) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := context.WithValue(r.Context(), goahttp.AcceptTypeKey, r.Header.Get("Accept")) + ctx = context.WithValue(ctx, goa.MethodKey, "create-project") + ctx = context.WithValue(ctx, goa.ServiceKey, "project-service") + payload, err := decodeRequest(r) + if err != nil { + if err := encodeError(ctx, w, err); err != nil { + errhandler(ctx, w, err) + } + return + } + res, err := endpoint(ctx, payload) + if err != nil { + if err := encodeError(ctx, w, err); err != nil { + errhandler(ctx, w, err) + } + return + } + if err := encodeResponse(ctx, w, res); err != nil { + errhandler(ctx, w, err) + } + }) +} + +// MountGetOneProjectHandler configures the mux to serve the "project-service" +// service "get-one-project" endpoint. +func MountGetOneProjectHandler(mux goahttp.Muxer, h http.Handler) { + f, ok := h.(http.HandlerFunc) + if !ok { + f = func(w http.ResponseWriter, r *http.Request) { + h.ServeHTTP(w, r) + } + } + mux.Handle("GET", "/projects/{id}", f) +} + +// NewGetOneProjectHandler creates a HTTP handler which loads the HTTP request +// and calls the "project-service" service "get-one-project" endpoint. +func NewGetOneProjectHandler( + endpoint goa.Endpoint, + mux goahttp.Muxer, + decoder func(*http.Request) goahttp.Decoder, + encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, + errhandler func(context.Context, http.ResponseWriter, error), + formatter func(ctx context.Context, err error) goahttp.Statuser, +) http.Handler { + var ( + decodeRequest = DecodeGetOneProjectRequest(mux, decoder) + encodeResponse = EncodeGetOneProjectResponse(encoder) + encodeError = EncodeGetOneProjectError(encoder, formatter) + ) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := context.WithValue(r.Context(), goahttp.AcceptTypeKey, r.Header.Get("Accept")) + ctx = context.WithValue(ctx, goa.MethodKey, "get-one-project") + ctx = context.WithValue(ctx, goa.ServiceKey, "project-service") + payload, err := decodeRequest(r) + if err != nil { + if err := encodeError(ctx, w, err); err != nil { + errhandler(ctx, w, err) + } + return + } + res, err := endpoint(ctx, payload) + if err != nil { + if err := encodeError(ctx, w, err); err != nil { + errhandler(ctx, w, err) + } + return + } + if err := encodeResponse(ctx, w, res); err != nil { + errhandler(ctx, w, err) + } + }) +} + +// MountUpdateProjectHandler configures the mux to serve the "project-service" +// service "update-project" endpoint. +func MountUpdateProjectHandler(mux goahttp.Muxer, h http.Handler) { + f, ok := h.(http.HandlerFunc) + if !ok { + f = func(w http.ResponseWriter, r *http.Request) { + h.ServeHTTP(w, r) + } + } + mux.Handle("PUT", "/projects/{id}", f) +} + +// NewUpdateProjectHandler creates a HTTP handler which loads the HTTP request +// and calls the "project-service" service "update-project" endpoint. +func NewUpdateProjectHandler( + endpoint goa.Endpoint, + mux goahttp.Muxer, + decoder func(*http.Request) goahttp.Decoder, + encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, + errhandler func(context.Context, http.ResponseWriter, error), + formatter func(ctx context.Context, err error) goahttp.Statuser, +) http.Handler { + var ( + decodeRequest = DecodeUpdateProjectRequest(mux, decoder) + encodeResponse = EncodeUpdateProjectResponse(encoder) + encodeError = EncodeUpdateProjectError(encoder, formatter) + ) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := context.WithValue(r.Context(), goahttp.AcceptTypeKey, r.Header.Get("Accept")) + ctx = context.WithValue(ctx, goa.MethodKey, "update-project") + ctx = context.WithValue(ctx, goa.ServiceKey, "project-service") + payload, err := decodeRequest(r) + if err != nil { + if err := encodeError(ctx, w, err); err != nil { + errhandler(ctx, w, err) + } + return + } + res, err := endpoint(ctx, payload) + if err != nil { + if err := encodeError(ctx, w, err); err != nil { + errhandler(ctx, w, err) + } + return + } + if err := encodeResponse(ctx, w, res); err != nil { + errhandler(ctx, w, err) + } + }) +} + +// MountDeleteProjectHandler configures the mux to serve the "project-service" +// service "delete-project" endpoint. +func MountDeleteProjectHandler(mux goahttp.Muxer, h http.Handler) { + f, ok := h.(http.HandlerFunc) + if !ok { + f = func(w http.ResponseWriter, r *http.Request) { + h.ServeHTTP(w, r) + } + } + mux.Handle("DELETE", "/projects/{id}", f) +} + +// NewDeleteProjectHandler creates a HTTP handler which loads the HTTP request +// and calls the "project-service" service "delete-project" endpoint. +func NewDeleteProjectHandler( + endpoint goa.Endpoint, + mux goahttp.Muxer, + decoder func(*http.Request) goahttp.Decoder, + encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, + errhandler func(context.Context, http.ResponseWriter, error), + formatter func(ctx context.Context, err error) goahttp.Statuser, +) http.Handler { + var ( + decodeRequest = DecodeDeleteProjectRequest(mux, decoder) + encodeResponse = EncodeDeleteProjectResponse(encoder) + encodeError = EncodeDeleteProjectError(encoder, formatter) + ) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := context.WithValue(r.Context(), goahttp.AcceptTypeKey, r.Header.Get("Accept")) + ctx = context.WithValue(ctx, goa.MethodKey, "delete-project") + ctx = context.WithValue(ctx, goa.ServiceKey, "project-service") + payload, err := decodeRequest(r) + if err != nil { + if err := encodeError(ctx, w, err); err != nil { + errhandler(ctx, w, err) + } + return + } + res, err := endpoint(ctx, payload) + if err != nil { + if err := encodeError(ctx, w, err); err != nil { + errhandler(ctx, w, err) + } + return + } + if err := encodeResponse(ctx, w, res); err != nil { + errhandler(ctx, w, err) + } + }) +} + +// MountReadyzHandler configures the mux to serve the "project-service" service +// "readyz" endpoint. +func MountReadyzHandler(mux goahttp.Muxer, h http.Handler) { + f, ok := h.(http.HandlerFunc) + if !ok { + f = func(w http.ResponseWriter, r *http.Request) { + h.ServeHTTP(w, r) + } + } + mux.Handle("GET", "/readyz", f) +} + +// NewReadyzHandler creates a HTTP handler which loads the HTTP request and +// calls the "project-service" service "readyz" endpoint. +func NewReadyzHandler( + endpoint goa.Endpoint, + mux goahttp.Muxer, + decoder func(*http.Request) goahttp.Decoder, + encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, + errhandler func(context.Context, http.ResponseWriter, error), + formatter func(ctx context.Context, err error) goahttp.Statuser, +) http.Handler { + var ( + encodeResponse = EncodeReadyzResponse(encoder) + encodeError = EncodeReadyzError(encoder, formatter) + ) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := context.WithValue(r.Context(), goahttp.AcceptTypeKey, r.Header.Get("Accept")) + ctx = context.WithValue(ctx, goa.MethodKey, "readyz") + ctx = context.WithValue(ctx, goa.ServiceKey, "project-service") + var err error + res, err := endpoint(ctx, nil) + if err != nil { + if err := encodeError(ctx, w, err); err != nil { + errhandler(ctx, w, err) + } + return + } + if err := encodeResponse(ctx, w, res); err != nil { + errhandler(ctx, w, err) + } + }) +} + +// MountLivezHandler configures the mux to serve the "project-service" service +// "livez" endpoint. +func MountLivezHandler(mux goahttp.Muxer, h http.Handler) { + f, ok := h.(http.HandlerFunc) + if !ok { + f = func(w http.ResponseWriter, r *http.Request) { + h.ServeHTTP(w, r) + } + } + mux.Handle("GET", "/livez", f) +} + +// NewLivezHandler creates a HTTP handler which loads the HTTP request and +// calls the "project-service" service "livez" endpoint. +func NewLivezHandler( + endpoint goa.Endpoint, + mux goahttp.Muxer, + decoder func(*http.Request) goahttp.Decoder, + encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, + errhandler func(context.Context, http.ResponseWriter, error), + formatter func(ctx context.Context, err error) goahttp.Statuser, +) http.Handler { + var ( + encodeResponse = EncodeLivezResponse(encoder) + encodeError = goahttp.ErrorEncoder(encoder, formatter) + ) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := context.WithValue(r.Context(), goahttp.AcceptTypeKey, r.Header.Get("Accept")) + ctx = context.WithValue(ctx, goa.MethodKey, "livez") + ctx = context.WithValue(ctx, goa.ServiceKey, "project-service") + var err error + res, err := endpoint(ctx, nil) + if err != nil { + if err := encodeError(ctx, w, err); err != nil { + errhandler(ctx, w, err) + } + return + } + if err := encodeResponse(ctx, w, res); err != nil { + errhandler(ctx, w, err) + } + }) +} + +// appendFS is a custom implementation of fs.FS that appends a specified prefix +// to the file paths before delegating the Open call to the underlying fs.FS. +type appendFS struct { + prefix string + fs http.FileSystem +} + +// Open opens the named file, appending the prefix to the file path before +// passing it to the underlying fs.FS. +func (s appendFS) Open(name string) (http.File, error) { + switch name { + case "/openapi.json": + name = "/openapi3.json" + } + return s.fs.Open(path.Join(s.prefix, name)) +} + +// appendPrefix returns a new fs.FS that appends the specified prefix to file paths +// before delegating to the provided embed.FS. +func appendPrefix(fsys http.FileSystem, prefix string) http.FileSystem { + return appendFS{prefix: prefix, fs: fsys} +} + +// MountGenHTTPOpenapi3JSON configures the mux to serve GET request made to +// "/openapi.json". +func MountGenHTTPOpenapi3JSON(mux goahttp.Muxer, h http.Handler) { + mux.Handle("GET", "/openapi.json", h.ServeHTTP) +} diff --git a/cmd/project-api/gen/http/project_service/server/types.go b/cmd/project-api/gen/http/project_service/server/types.go new file mode 100644 index 0000000..998b2d3 --- /dev/null +++ b/cmd/project-api/gen/http/project_service/server/types.go @@ -0,0 +1,741 @@ +// Code generated by goa v3.21.1, DO NOT EDIT. +// +// project-service HTTP server types +// +// Command: +// $ goa gen +// github.com/linuxfoundation/lfx-v2-project-service/cmd/project-api/design + +package server + +import ( + projectservice "github.com/linuxfoundation/lfx-v2-project-service/cmd/project-api/gen/project_service" + goa "goa.design/goa/v3/pkg" +) + +// CreateProjectRequestBody is the type of the "project-service" service +// "create-project" endpoint HTTP request body. +type CreateProjectRequestBody struct { + // Project slug, a short slugified name of the project + Slug *string `form:"slug,omitempty" json:"slug,omitempty" xml:"slug,omitempty"` + // A description of the project + Description *string `form:"description,omitempty" json:"description,omitempty" xml:"description,omitempty"` + // The pretty name of the project + Name *string `form:"name,omitempty" json:"name,omitempty" xml:"name,omitempty"` + // Whether the project is public + Public *bool `form:"public,omitempty" json:"public,omitempty" xml:"public,omitempty"` + // The UID of the parent project, should be empty if there is none + ParentUID *string `form:"parent_uid,omitempty" json:"parent_uid,omitempty" xml:"parent_uid,omitempty"` + // A list of project auditors by their user IDs + Auditors []string `form:"auditors,omitempty" json:"auditors,omitempty" xml:"auditors,omitempty"` + // A list of project writers by their user IDs + Writers []string `form:"writers,omitempty" json:"writers,omitempty" xml:"writers,omitempty"` +} + +// UpdateProjectRequestBody is the type of the "project-service" service +// "update-project" endpoint HTTP request body. +type UpdateProjectRequestBody struct { + // Project slug, a short slugified name of the project + Slug *string `form:"slug,omitempty" json:"slug,omitempty" xml:"slug,omitempty"` + // A description of the project + Description *string `form:"description,omitempty" json:"description,omitempty" xml:"description,omitempty"` + // The pretty name of the project + Name *string `form:"name,omitempty" json:"name,omitempty" xml:"name,omitempty"` + // Whether the project is public + Public *bool `form:"public,omitempty" json:"public,omitempty" xml:"public,omitempty"` + // The UID of the parent project, should be empty if there is none + ParentUID *string `form:"parent_uid,omitempty" json:"parent_uid,omitempty" xml:"parent_uid,omitempty"` + // A list of project auditors by their user IDs + Auditors []string `form:"auditors,omitempty" json:"auditors,omitempty" xml:"auditors,omitempty"` + // A list of project writers by their user IDs + Writers []string `form:"writers,omitempty" json:"writers,omitempty" xml:"writers,omitempty"` +} + +// GetProjectsResponseBody is the type of the "project-service" service +// "get-projects" endpoint HTTP response body. +type GetProjectsResponseBody struct { + // Resources found + Projects []*ProjectResponseBody `form:"projects" json:"projects" xml:"projects"` +} + +// CreateProjectResponseBody is the type of the "project-service" service +// "create-project" endpoint HTTP response body. +type CreateProjectResponseBody struct { + // Project ID -- v2 id, not related to v1 id directly + ID *string `form:"id,omitempty" json:"id,omitempty" xml:"id,omitempty"` + // Project slug, a short slugified name of the project + Slug *string `form:"slug,omitempty" json:"slug,omitempty" xml:"slug,omitempty"` + // A description of the project + Description *string `form:"description,omitempty" json:"description,omitempty" xml:"description,omitempty"` + // The pretty name of the project + Name *string `form:"name,omitempty" json:"name,omitempty" xml:"name,omitempty"` + // Whether the project is public + Public *bool `form:"public,omitempty" json:"public,omitempty" xml:"public,omitempty"` + // The UID of the parent project, should be empty if there is none + ParentUID *string `form:"parent_uid,omitempty" json:"parent_uid,omitempty" xml:"parent_uid,omitempty"` + // A list of project auditors by their user IDs + Auditors []string `form:"auditors,omitempty" json:"auditors,omitempty" xml:"auditors,omitempty"` + // A list of project writers by their user IDs + Writers []string `form:"writers,omitempty" json:"writers,omitempty" xml:"writers,omitempty"` +} + +// GetOneProjectResponseBody is the type of the "project-service" service +// "get-one-project" endpoint HTTP response body. +type GetOneProjectResponseBody ProjectResponseBody + +// UpdateProjectResponseBody is the type of the "project-service" service +// "update-project" endpoint HTTP response body. +type UpdateProjectResponseBody struct { + // Project ID -- v2 id, not related to v1 id directly + ID *string `form:"id,omitempty" json:"id,omitempty" xml:"id,omitempty"` + // Project slug, a short slugified name of the project + Slug *string `form:"slug,omitempty" json:"slug,omitempty" xml:"slug,omitempty"` + // A description of the project + Description *string `form:"description,omitempty" json:"description,omitempty" xml:"description,omitempty"` + // The pretty name of the project + Name *string `form:"name,omitempty" json:"name,omitempty" xml:"name,omitempty"` + // Whether the project is public + Public *bool `form:"public,omitempty" json:"public,omitempty" xml:"public,omitempty"` + // The UID of the parent project, should be empty if there is none + ParentUID *string `form:"parent_uid,omitempty" json:"parent_uid,omitempty" xml:"parent_uid,omitempty"` + // A list of project auditors by their user IDs + Auditors []string `form:"auditors,omitempty" json:"auditors,omitempty" xml:"auditors,omitempty"` + // A list of project writers by their user IDs + Writers []string `form:"writers,omitempty" json:"writers,omitempty" xml:"writers,omitempty"` +} + +// GetProjectsBadRequestResponseBody is the type of the "project-service" +// service "get-projects" endpoint HTTP response body for the "BadRequest" +// error. +type GetProjectsBadRequestResponseBody struct { + // HTTP status code + Code string `form:"code" json:"code" xml:"code"` + // Error message + Message string `form:"message" json:"message" xml:"message"` +} + +// GetProjectsInternalServerErrorResponseBody is the type of the +// "project-service" service "get-projects" endpoint HTTP response body for the +// "InternalServerError" error. +type GetProjectsInternalServerErrorResponseBody struct { + // HTTP status code + Code string `form:"code" json:"code" xml:"code"` + // Error message + Message string `form:"message" json:"message" xml:"message"` +} + +// GetProjectsServiceUnavailableResponseBody is the type of the +// "project-service" service "get-projects" endpoint HTTP response body for the +// "ServiceUnavailable" error. +type GetProjectsServiceUnavailableResponseBody struct { + // HTTP status code + Code string `form:"code" json:"code" xml:"code"` + // Error message + Message string `form:"message" json:"message" xml:"message"` +} + +// CreateProjectBadRequestResponseBody is the type of the "project-service" +// service "create-project" endpoint HTTP response body for the "BadRequest" +// error. +type CreateProjectBadRequestResponseBody struct { + // HTTP status code + Code string `form:"code" json:"code" xml:"code"` + // Error message + Message string `form:"message" json:"message" xml:"message"` +} + +// CreateProjectConflictResponseBody is the type of the "project-service" +// service "create-project" endpoint HTTP response body for the "Conflict" +// error. +type CreateProjectConflictResponseBody struct { + // HTTP status code + Code string `form:"code" json:"code" xml:"code"` + // Error message + Message string `form:"message" json:"message" xml:"message"` +} + +// CreateProjectInternalServerErrorResponseBody is the type of the +// "project-service" service "create-project" endpoint HTTP response body for +// the "InternalServerError" error. +type CreateProjectInternalServerErrorResponseBody struct { + // HTTP status code + Code string `form:"code" json:"code" xml:"code"` + // Error message + Message string `form:"message" json:"message" xml:"message"` +} + +// CreateProjectServiceUnavailableResponseBody is the type of the +// "project-service" service "create-project" endpoint HTTP response body for +// the "ServiceUnavailable" error. +type CreateProjectServiceUnavailableResponseBody struct { + // HTTP status code + Code string `form:"code" json:"code" xml:"code"` + // Error message + Message string `form:"message" json:"message" xml:"message"` +} + +// GetOneProjectInternalServerErrorResponseBody is the type of the +// "project-service" service "get-one-project" endpoint HTTP response body for +// the "InternalServerError" error. +type GetOneProjectInternalServerErrorResponseBody struct { + // HTTP status code + Code string `form:"code" json:"code" xml:"code"` + // Error message + Message string `form:"message" json:"message" xml:"message"` +} + +// GetOneProjectNotFoundResponseBody is the type of the "project-service" +// service "get-one-project" endpoint HTTP response body for the "NotFound" +// error. +type GetOneProjectNotFoundResponseBody struct { + // HTTP status code + Code string `form:"code" json:"code" xml:"code"` + // Error message + Message string `form:"message" json:"message" xml:"message"` +} + +// GetOneProjectServiceUnavailableResponseBody is the type of the +// "project-service" service "get-one-project" endpoint HTTP response body for +// the "ServiceUnavailable" error. +type GetOneProjectServiceUnavailableResponseBody struct { + // HTTP status code + Code string `form:"code" json:"code" xml:"code"` + // Error message + Message string `form:"message" json:"message" xml:"message"` +} + +// UpdateProjectBadRequestResponseBody is the type of the "project-service" +// service "update-project" endpoint HTTP response body for the "BadRequest" +// error. +type UpdateProjectBadRequestResponseBody struct { + // HTTP status code + Code string `form:"code" json:"code" xml:"code"` + // Error message + Message string `form:"message" json:"message" xml:"message"` +} + +// UpdateProjectInternalServerErrorResponseBody is the type of the +// "project-service" service "update-project" endpoint HTTP response body for +// the "InternalServerError" error. +type UpdateProjectInternalServerErrorResponseBody struct { + // HTTP status code + Code string `form:"code" json:"code" xml:"code"` + // Error message + Message string `form:"message" json:"message" xml:"message"` +} + +// UpdateProjectNotFoundResponseBody is the type of the "project-service" +// service "update-project" endpoint HTTP response body for the "NotFound" +// error. +type UpdateProjectNotFoundResponseBody struct { + // HTTP status code + Code string `form:"code" json:"code" xml:"code"` + // Error message + Message string `form:"message" json:"message" xml:"message"` +} + +// UpdateProjectServiceUnavailableResponseBody is the type of the +// "project-service" service "update-project" endpoint HTTP response body for +// the "ServiceUnavailable" error. +type UpdateProjectServiceUnavailableResponseBody struct { + // HTTP status code + Code string `form:"code" json:"code" xml:"code"` + // Error message + Message string `form:"message" json:"message" xml:"message"` +} + +// DeleteProjectBadRequestResponseBody is the type of the "project-service" +// service "delete-project" endpoint HTTP response body for the "BadRequest" +// error. +type DeleteProjectBadRequestResponseBody struct { + // HTTP status code + Code string `form:"code" json:"code" xml:"code"` + // Error message + Message string `form:"message" json:"message" xml:"message"` +} + +// DeleteProjectInternalServerErrorResponseBody is the type of the +// "project-service" service "delete-project" endpoint HTTP response body for +// the "InternalServerError" error. +type DeleteProjectInternalServerErrorResponseBody struct { + // HTTP status code + Code string `form:"code" json:"code" xml:"code"` + // Error message + Message string `form:"message" json:"message" xml:"message"` +} + +// DeleteProjectNotFoundResponseBody is the type of the "project-service" +// service "delete-project" endpoint HTTP response body for the "NotFound" +// error. +type DeleteProjectNotFoundResponseBody struct { + // HTTP status code + Code string `form:"code" json:"code" xml:"code"` + // Error message + Message string `form:"message" json:"message" xml:"message"` +} + +// DeleteProjectServiceUnavailableResponseBody is the type of the +// "project-service" service "delete-project" endpoint HTTP response body for +// the "ServiceUnavailable" error. +type DeleteProjectServiceUnavailableResponseBody struct { + // HTTP status code + Code string `form:"code" json:"code" xml:"code"` + // Error message + Message string `form:"message" json:"message" xml:"message"` +} + +// ReadyzServiceUnavailableResponseBody is the type of the "project-service" +// service "readyz" endpoint HTTP response body for the "ServiceUnavailable" +// error. +type ReadyzServiceUnavailableResponseBody struct { + // HTTP status code + Code string `form:"code" json:"code" xml:"code"` + // Error message + Message string `form:"message" json:"message" xml:"message"` +} + +// ProjectResponseBody is used to define fields on response body types. +type ProjectResponseBody struct { + // Project ID -- v2 id, not related to v1 id directly + ID *string `form:"id,omitempty" json:"id,omitempty" xml:"id,omitempty"` + // Project slug, a short slugified name of the project + Slug *string `form:"slug,omitempty" json:"slug,omitempty" xml:"slug,omitempty"` + // A description of the project + Description *string `form:"description,omitempty" json:"description,omitempty" xml:"description,omitempty"` + // The pretty name of the project + Name *string `form:"name,omitempty" json:"name,omitempty" xml:"name,omitempty"` + // Whether the project is public + Public *bool `form:"public,omitempty" json:"public,omitempty" xml:"public,omitempty"` + // The UID of the parent project, should be empty if there is none + ParentUID *string `form:"parent_uid,omitempty" json:"parent_uid,omitempty" xml:"parent_uid,omitempty"` + // A list of project auditors by their user IDs + Auditors []string `form:"auditors,omitempty" json:"auditors,omitempty" xml:"auditors,omitempty"` + // A list of project writers by their user IDs + Writers []string `form:"writers,omitempty" json:"writers,omitempty" xml:"writers,omitempty"` +} + +// NewGetProjectsResponseBody builds the HTTP response body from the result of +// the "get-projects" endpoint of the "project-service" service. +func NewGetProjectsResponseBody(res *projectservice.GetProjectsResult) *GetProjectsResponseBody { + body := &GetProjectsResponseBody{} + if res.Projects != nil { + body.Projects = make([]*ProjectResponseBody, len(res.Projects)) + for i, val := range res.Projects { + body.Projects[i] = marshalProjectserviceProjectToProjectResponseBody(val) + } + } else { + body.Projects = []*ProjectResponseBody{} + } + return body +} + +// NewCreateProjectResponseBody builds the HTTP response body from the result +// of the "create-project" endpoint of the "project-service" service. +func NewCreateProjectResponseBody(res *projectservice.Project) *CreateProjectResponseBody { + body := &CreateProjectResponseBody{ + ID: res.ID, + Slug: res.Slug, + Description: res.Description, + Name: res.Name, + Public: res.Public, + ParentUID: res.ParentUID, + } + if res.Auditors != nil { + body.Auditors = make([]string, len(res.Auditors)) + for i, val := range res.Auditors { + body.Auditors[i] = val + } + } + if res.Writers != nil { + body.Writers = make([]string, len(res.Writers)) + for i, val := range res.Writers { + body.Writers[i] = val + } + } + return body +} + +// NewGetOneProjectResponseBody builds the HTTP response body from the result +// of the "get-one-project" endpoint of the "project-service" service. +func NewGetOneProjectResponseBody(res *projectservice.GetOneProjectResult) *GetOneProjectResponseBody { + body := &GetOneProjectResponseBody{ + ID: res.Project.ID, + Slug: res.Project.Slug, + Description: res.Project.Description, + Name: res.Project.Name, + Public: res.Project.Public, + ParentUID: res.Project.ParentUID, + } + if res.Project.Auditors != nil { + body.Auditors = make([]string, len(res.Project.Auditors)) + for i, val := range res.Project.Auditors { + body.Auditors[i] = val + } + } + if res.Project.Writers != nil { + body.Writers = make([]string, len(res.Project.Writers)) + for i, val := range res.Project.Writers { + body.Writers[i] = val + } + } + return body +} + +// NewUpdateProjectResponseBody builds the HTTP response body from the result +// of the "update-project" endpoint of the "project-service" service. +func NewUpdateProjectResponseBody(res *projectservice.Project) *UpdateProjectResponseBody { + body := &UpdateProjectResponseBody{ + ID: res.ID, + Slug: res.Slug, + Description: res.Description, + Name: res.Name, + Public: res.Public, + ParentUID: res.ParentUID, + } + if res.Auditors != nil { + body.Auditors = make([]string, len(res.Auditors)) + for i, val := range res.Auditors { + body.Auditors[i] = val + } + } + if res.Writers != nil { + body.Writers = make([]string, len(res.Writers)) + for i, val := range res.Writers { + body.Writers[i] = val + } + } + return body +} + +// NewGetProjectsBadRequestResponseBody builds the HTTP response body from the +// result of the "get-projects" endpoint of the "project-service" service. +func NewGetProjectsBadRequestResponseBody(res *projectservice.BadRequestError) *GetProjectsBadRequestResponseBody { + body := &GetProjectsBadRequestResponseBody{ + Code: res.Code, + Message: res.Message, + } + return body +} + +// NewGetProjectsInternalServerErrorResponseBody builds the HTTP response body +// from the result of the "get-projects" endpoint of the "project-service" +// service. +func NewGetProjectsInternalServerErrorResponseBody(res *projectservice.InternalServerError) *GetProjectsInternalServerErrorResponseBody { + body := &GetProjectsInternalServerErrorResponseBody{ + Code: res.Code, + Message: res.Message, + } + return body +} + +// NewGetProjectsServiceUnavailableResponseBody builds the HTTP response body +// from the result of the "get-projects" endpoint of the "project-service" +// service. +func NewGetProjectsServiceUnavailableResponseBody(res *projectservice.ServiceUnavailableError) *GetProjectsServiceUnavailableResponseBody { + body := &GetProjectsServiceUnavailableResponseBody{ + Code: res.Code, + Message: res.Message, + } + return body +} + +// NewCreateProjectBadRequestResponseBody builds the HTTP response body from +// the result of the "create-project" endpoint of the "project-service" service. +func NewCreateProjectBadRequestResponseBody(res *projectservice.BadRequestError) *CreateProjectBadRequestResponseBody { + body := &CreateProjectBadRequestResponseBody{ + Code: res.Code, + Message: res.Message, + } + return body +} + +// NewCreateProjectConflictResponseBody builds the HTTP response body from the +// result of the "create-project" endpoint of the "project-service" service. +func NewCreateProjectConflictResponseBody(res *projectservice.ConflictError) *CreateProjectConflictResponseBody { + body := &CreateProjectConflictResponseBody{ + Code: res.Code, + Message: res.Message, + } + return body +} + +// NewCreateProjectInternalServerErrorResponseBody builds the HTTP response +// body from the result of the "create-project" endpoint of the +// "project-service" service. +func NewCreateProjectInternalServerErrorResponseBody(res *projectservice.InternalServerError) *CreateProjectInternalServerErrorResponseBody { + body := &CreateProjectInternalServerErrorResponseBody{ + Code: res.Code, + Message: res.Message, + } + return body +} + +// NewCreateProjectServiceUnavailableResponseBody builds the HTTP response body +// from the result of the "create-project" endpoint of the "project-service" +// service. +func NewCreateProjectServiceUnavailableResponseBody(res *projectservice.ServiceUnavailableError) *CreateProjectServiceUnavailableResponseBody { + body := &CreateProjectServiceUnavailableResponseBody{ + Code: res.Code, + Message: res.Message, + } + return body +} + +// NewGetOneProjectInternalServerErrorResponseBody builds the HTTP response +// body from the result of the "get-one-project" endpoint of the +// "project-service" service. +func NewGetOneProjectInternalServerErrorResponseBody(res *projectservice.InternalServerError) *GetOneProjectInternalServerErrorResponseBody { + body := &GetOneProjectInternalServerErrorResponseBody{ + Code: res.Code, + Message: res.Message, + } + return body +} + +// NewGetOneProjectNotFoundResponseBody builds the HTTP response body from the +// result of the "get-one-project" endpoint of the "project-service" service. +func NewGetOneProjectNotFoundResponseBody(res *projectservice.NotFoundError) *GetOneProjectNotFoundResponseBody { + body := &GetOneProjectNotFoundResponseBody{ + Code: res.Code, + Message: res.Message, + } + return body +} + +// NewGetOneProjectServiceUnavailableResponseBody builds the HTTP response body +// from the result of the "get-one-project" endpoint of the "project-service" +// service. +func NewGetOneProjectServiceUnavailableResponseBody(res *projectservice.ServiceUnavailableError) *GetOneProjectServiceUnavailableResponseBody { + body := &GetOneProjectServiceUnavailableResponseBody{ + Code: res.Code, + Message: res.Message, + } + return body +} + +// NewUpdateProjectBadRequestResponseBody builds the HTTP response body from +// the result of the "update-project" endpoint of the "project-service" service. +func NewUpdateProjectBadRequestResponseBody(res *projectservice.BadRequestError) *UpdateProjectBadRequestResponseBody { + body := &UpdateProjectBadRequestResponseBody{ + Code: res.Code, + Message: res.Message, + } + return body +} + +// NewUpdateProjectInternalServerErrorResponseBody builds the HTTP response +// body from the result of the "update-project" endpoint of the +// "project-service" service. +func NewUpdateProjectInternalServerErrorResponseBody(res *projectservice.InternalServerError) *UpdateProjectInternalServerErrorResponseBody { + body := &UpdateProjectInternalServerErrorResponseBody{ + Code: res.Code, + Message: res.Message, + } + return body +} + +// NewUpdateProjectNotFoundResponseBody builds the HTTP response body from the +// result of the "update-project" endpoint of the "project-service" service. +func NewUpdateProjectNotFoundResponseBody(res *projectservice.NotFoundError) *UpdateProjectNotFoundResponseBody { + body := &UpdateProjectNotFoundResponseBody{ + Code: res.Code, + Message: res.Message, + } + return body +} + +// NewUpdateProjectServiceUnavailableResponseBody builds the HTTP response body +// from the result of the "update-project" endpoint of the "project-service" +// service. +func NewUpdateProjectServiceUnavailableResponseBody(res *projectservice.ServiceUnavailableError) *UpdateProjectServiceUnavailableResponseBody { + body := &UpdateProjectServiceUnavailableResponseBody{ + Code: res.Code, + Message: res.Message, + } + return body +} + +// NewDeleteProjectBadRequestResponseBody builds the HTTP response body from +// the result of the "delete-project" endpoint of the "project-service" service. +func NewDeleteProjectBadRequestResponseBody(res *projectservice.BadRequestError) *DeleteProjectBadRequestResponseBody { + body := &DeleteProjectBadRequestResponseBody{ + Code: res.Code, + Message: res.Message, + } + return body +} + +// NewDeleteProjectInternalServerErrorResponseBody builds the HTTP response +// body from the result of the "delete-project" endpoint of the +// "project-service" service. +func NewDeleteProjectInternalServerErrorResponseBody(res *projectservice.InternalServerError) *DeleteProjectInternalServerErrorResponseBody { + body := &DeleteProjectInternalServerErrorResponseBody{ + Code: res.Code, + Message: res.Message, + } + return body +} + +// NewDeleteProjectNotFoundResponseBody builds the HTTP response body from the +// result of the "delete-project" endpoint of the "project-service" service. +func NewDeleteProjectNotFoundResponseBody(res *projectservice.NotFoundError) *DeleteProjectNotFoundResponseBody { + body := &DeleteProjectNotFoundResponseBody{ + Code: res.Code, + Message: res.Message, + } + return body +} + +// NewDeleteProjectServiceUnavailableResponseBody builds the HTTP response body +// from the result of the "delete-project" endpoint of the "project-service" +// service. +func NewDeleteProjectServiceUnavailableResponseBody(res *projectservice.ServiceUnavailableError) *DeleteProjectServiceUnavailableResponseBody { + body := &DeleteProjectServiceUnavailableResponseBody{ + Code: res.Code, + Message: res.Message, + } + return body +} + +// NewReadyzServiceUnavailableResponseBody builds the HTTP response body from +// the result of the "readyz" endpoint of the "project-service" service. +func NewReadyzServiceUnavailableResponseBody(res *projectservice.ServiceUnavailableError) *ReadyzServiceUnavailableResponseBody { + body := &ReadyzServiceUnavailableResponseBody{ + Code: res.Code, + Message: res.Message, + } + return body +} + +// NewGetProjectsPayload builds a project-service service get-projects endpoint +// payload. +func NewGetProjectsPayload(version *string, bearerToken *string) *projectservice.GetProjectsPayload { + v := &projectservice.GetProjectsPayload{} + v.Version = version + v.BearerToken = bearerToken + + return v +} + +// NewCreateProjectPayload builds a project-service service create-project +// endpoint payload. +func NewCreateProjectPayload(body *CreateProjectRequestBody, version *string, bearerToken *string) *projectservice.CreateProjectPayload { + v := &projectservice.CreateProjectPayload{ + Slug: *body.Slug, + Description: *body.Description, + Name: *body.Name, + Public: body.Public, + ParentUID: body.ParentUID, + } + if body.Auditors != nil { + v.Auditors = make([]string, len(body.Auditors)) + for i, val := range body.Auditors { + v.Auditors[i] = val + } + } + if body.Writers != nil { + v.Writers = make([]string, len(body.Writers)) + for i, val := range body.Writers { + v.Writers[i] = val + } + } + v.Version = version + v.BearerToken = bearerToken + + return v +} + +// NewGetOneProjectPayload builds a project-service service get-one-project +// endpoint payload. +func NewGetOneProjectPayload(id string, version *string, bearerToken *string) *projectservice.GetOneProjectPayload { + v := &projectservice.GetOneProjectPayload{} + v.ID = &id + v.Version = version + v.BearerToken = bearerToken + + return v +} + +// NewUpdateProjectPayload builds a project-service service update-project +// endpoint payload. +func NewUpdateProjectPayload(body *UpdateProjectRequestBody, id string, version *string, bearerToken *string, etag *string) *projectservice.UpdateProjectPayload { + v := &projectservice.UpdateProjectPayload{ + Slug: *body.Slug, + Description: *body.Description, + Name: *body.Name, + Public: body.Public, + ParentUID: body.ParentUID, + } + if body.Auditors != nil { + v.Auditors = make([]string, len(body.Auditors)) + for i, val := range body.Auditors { + v.Auditors[i] = val + } + } + if body.Writers != nil { + v.Writers = make([]string, len(body.Writers)) + for i, val := range body.Writers { + v.Writers[i] = val + } + } + v.ID = &id + v.Version = version + v.BearerToken = bearerToken + v.Etag = etag + + return v +} + +// NewDeleteProjectPayload builds a project-service service delete-project +// endpoint payload. +func NewDeleteProjectPayload(id string, version *string, bearerToken *string, etag *string) *projectservice.DeleteProjectPayload { + v := &projectservice.DeleteProjectPayload{} + v.ID = &id + v.Version = version + v.BearerToken = bearerToken + v.Etag = etag + + return v +} + +// ValidateCreateProjectRequestBody runs the validations defined on +// Create-ProjectRequestBody +func ValidateCreateProjectRequestBody(body *CreateProjectRequestBody) (err error) { + if body.Slug == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("slug", "body")) + } + if body.Description == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("description", "body")) + } + if body.Name == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("name", "body")) + } + if body.Slug != nil { + err = goa.MergeErrors(err, goa.ValidateFormat("body.slug", *body.Slug, goa.FormatRegexp)) + } + if body.Slug != nil { + err = goa.MergeErrors(err, goa.ValidatePattern("body.slug", *body.Slug, "^[a-z][a-z0-9_\\-]*[a-z0-9]$")) + } + return +} + +// ValidateUpdateProjectRequestBody runs the validations defined on +// Update-ProjectRequestBody +func ValidateUpdateProjectRequestBody(body *UpdateProjectRequestBody) (err error) { + if body.Slug == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("slug", "body")) + } + if body.Description == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("description", "body")) + } + if body.Name == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("name", "body")) + } + if body.Slug != nil { + err = goa.MergeErrors(err, goa.ValidateFormat("body.slug", *body.Slug, goa.FormatRegexp)) + } + if body.Slug != nil { + err = goa.MergeErrors(err, goa.ValidatePattern("body.slug", *body.Slug, "^[a-z][a-z0-9_\\-]*[a-z0-9]$")) + } + return +} diff --git a/cmd/project-api/gen/project_service/client.go b/cmd/project-api/gen/project_service/client.go new file mode 100644 index 0000000..a00e374 --- /dev/null +++ b/cmd/project-api/gen/project_service/client.go @@ -0,0 +1,141 @@ +// Code generated by goa v3.21.1, DO NOT EDIT. +// +// project-service client +// +// Command: +// $ goa gen +// github.com/linuxfoundation/lfx-v2-project-service/cmd/project-api/design + +package projectservice + +import ( + "context" + + goa "goa.design/goa/v3/pkg" +) + +// Client is the "project-service" service client. +type Client struct { + GetProjectsEndpoint goa.Endpoint + CreateProjectEndpoint goa.Endpoint + GetOneProjectEndpoint goa.Endpoint + UpdateProjectEndpoint goa.Endpoint + DeleteProjectEndpoint goa.Endpoint + ReadyzEndpoint goa.Endpoint + LivezEndpoint goa.Endpoint +} + +// NewClient initializes a "project-service" service client given the endpoints. +func NewClient(getProjects, createProject, getOneProject, updateProject, deleteProject, readyz, livez goa.Endpoint) *Client { + return &Client{ + GetProjectsEndpoint: getProjects, + CreateProjectEndpoint: createProject, + GetOneProjectEndpoint: getOneProject, + UpdateProjectEndpoint: updateProject, + DeleteProjectEndpoint: deleteProject, + ReadyzEndpoint: readyz, + LivezEndpoint: livez, + } +} + +// GetProjects calls the "get-projects" endpoint of the "project-service" +// service. +// GetProjects may return the following errors: +// - "BadRequest" (type *BadRequestError): Bad request +// - "InternalServerError" (type *InternalServerError): Internal server error +// - "ServiceUnavailable" (type *ServiceUnavailableError): Service unavailable +// - error: internal error +func (c *Client) GetProjects(ctx context.Context, p *GetProjectsPayload) (res *GetProjectsResult, err error) { + var ires any + ires, err = c.GetProjectsEndpoint(ctx, p) + if err != nil { + return + } + return ires.(*GetProjectsResult), nil +} + +// CreateProject calls the "create-project" endpoint of the "project-service" +// service. +// CreateProject may return the following errors: +// - "BadRequest" (type *BadRequestError): Bad request +// - "Conflict" (type *ConflictError): Conflict +// - "InternalServerError" (type *InternalServerError): Internal server error +// - "ServiceUnavailable" (type *ServiceUnavailableError): Service unavailable +// - error: internal error +func (c *Client) CreateProject(ctx context.Context, p *CreateProjectPayload) (res *Project, err error) { + var ires any + ires, err = c.CreateProjectEndpoint(ctx, p) + if err != nil { + return + } + return ires.(*Project), nil +} + +// GetOneProject calls the "get-one-project" endpoint of the "project-service" +// service. +// GetOneProject may return the following errors: +// - "NotFound" (type *NotFoundError): Resource not found +// - "InternalServerError" (type *InternalServerError): Internal server error +// - "ServiceUnavailable" (type *ServiceUnavailableError): Service unavailable +// - error: internal error +func (c *Client) GetOneProject(ctx context.Context, p *GetOneProjectPayload) (res *GetOneProjectResult, err error) { + var ires any + ires, err = c.GetOneProjectEndpoint(ctx, p) + if err != nil { + return + } + return ires.(*GetOneProjectResult), nil +} + +// UpdateProject calls the "update-project" endpoint of the "project-service" +// service. +// UpdateProject may return the following errors: +// - "BadRequest" (type *BadRequestError): Bad request +// - "NotFound" (type *NotFoundError): Resource not found +// - "InternalServerError" (type *InternalServerError): Internal server error +// - "ServiceUnavailable" (type *ServiceUnavailableError): Service unavailable +// - error: internal error +func (c *Client) UpdateProject(ctx context.Context, p *UpdateProjectPayload) (res *Project, err error) { + var ires any + ires, err = c.UpdateProjectEndpoint(ctx, p) + if err != nil { + return + } + return ires.(*Project), nil +} + +// DeleteProject calls the "delete-project" endpoint of the "project-service" +// service. +// DeleteProject may return the following errors: +// - "NotFound" (type *NotFoundError): Resource not found +// - "BadRequest" (type *BadRequestError): Bad request +// - "InternalServerError" (type *InternalServerError): Internal server error +// - "ServiceUnavailable" (type *ServiceUnavailableError): Service unavailable +// - error: internal error +func (c *Client) DeleteProject(ctx context.Context, p *DeleteProjectPayload) (err error) { + _, err = c.DeleteProjectEndpoint(ctx, p) + return +} + +// Readyz calls the "readyz" endpoint of the "project-service" service. +// Readyz may return the following errors: +// - "ServiceUnavailable" (type *ServiceUnavailableError): Service is unavailable +// - error: internal error +func (c *Client) Readyz(ctx context.Context) (res []byte, err error) { + var ires any + ires, err = c.ReadyzEndpoint(ctx, nil) + if err != nil { + return + } + return ires.([]byte), nil +} + +// Livez calls the "livez" endpoint of the "project-service" service. +func (c *Client) Livez(ctx context.Context) (res []byte, err error) { + var ires any + ires, err = c.LivezEndpoint(ctx, nil) + if err != nil { + return + } + return ires.([]byte), nil +} diff --git a/cmd/project-api/gen/project_service/endpoints.go b/cmd/project-api/gen/project_service/endpoints.go new file mode 100644 index 0000000..24bf6e1 --- /dev/null +++ b/cmd/project-api/gen/project_service/endpoints.go @@ -0,0 +1,186 @@ +// Code generated by goa v3.21.1, DO NOT EDIT. +// +// project-service endpoints +// +// Command: +// $ goa gen +// github.com/linuxfoundation/lfx-v2-project-service/cmd/project-api/design + +package projectservice + +import ( + "context" + + goa "goa.design/goa/v3/pkg" + "goa.design/goa/v3/security" +) + +// Endpoints wraps the "project-service" service endpoints. +type Endpoints struct { + GetProjects goa.Endpoint + CreateProject goa.Endpoint + GetOneProject goa.Endpoint + UpdateProject goa.Endpoint + DeleteProject goa.Endpoint + Readyz goa.Endpoint + Livez goa.Endpoint +} + +// NewEndpoints wraps the methods of the "project-service" service with +// endpoints. +func NewEndpoints(s Service) *Endpoints { + // Casting service to Auther interface + a := s.(Auther) + return &Endpoints{ + GetProjects: NewGetProjectsEndpoint(s, a.JWTAuth), + CreateProject: NewCreateProjectEndpoint(s, a.JWTAuth), + GetOneProject: NewGetOneProjectEndpoint(s, a.JWTAuth), + UpdateProject: NewUpdateProjectEndpoint(s, a.JWTAuth), + DeleteProject: NewDeleteProjectEndpoint(s, a.JWTAuth), + Readyz: NewReadyzEndpoint(s), + Livez: NewLivezEndpoint(s), + } +} + +// Use applies the given middleware to all the "project-service" service +// endpoints. +func (e *Endpoints) Use(m func(goa.Endpoint) goa.Endpoint) { + e.GetProjects = m(e.GetProjects) + e.CreateProject = m(e.CreateProject) + e.GetOneProject = m(e.GetOneProject) + e.UpdateProject = m(e.UpdateProject) + e.DeleteProject = m(e.DeleteProject) + e.Readyz = m(e.Readyz) + e.Livez = m(e.Livez) +} + +// NewGetProjectsEndpoint returns an endpoint function that calls the method +// "get-projects" of service "project-service". +func NewGetProjectsEndpoint(s Service, authJWTFn security.AuthJWTFunc) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + p := req.(*GetProjectsPayload) + var err error + sc := security.JWTScheme{ + Name: "jwt", + Scopes: []string{}, + RequiredScopes: []string{}, + } + var token string + if p.BearerToken != nil { + token = *p.BearerToken + } + ctx, err = authJWTFn(ctx, token, &sc) + if err != nil { + return nil, err + } + return s.GetProjects(ctx, p) + } +} + +// NewCreateProjectEndpoint returns an endpoint function that calls the method +// "create-project" of service "project-service". +func NewCreateProjectEndpoint(s Service, authJWTFn security.AuthJWTFunc) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + p := req.(*CreateProjectPayload) + var err error + sc := security.JWTScheme{ + Name: "jwt", + Scopes: []string{}, + RequiredScopes: []string{}, + } + var token string + if p.BearerToken != nil { + token = *p.BearerToken + } + ctx, err = authJWTFn(ctx, token, &sc) + if err != nil { + return nil, err + } + return s.CreateProject(ctx, p) + } +} + +// NewGetOneProjectEndpoint returns an endpoint function that calls the method +// "get-one-project" of service "project-service". +func NewGetOneProjectEndpoint(s Service, authJWTFn security.AuthJWTFunc) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + p := req.(*GetOneProjectPayload) + var err error + sc := security.JWTScheme{ + Name: "jwt", + Scopes: []string{}, + RequiredScopes: []string{}, + } + var token string + if p.BearerToken != nil { + token = *p.BearerToken + } + ctx, err = authJWTFn(ctx, token, &sc) + if err != nil { + return nil, err + } + return s.GetOneProject(ctx, p) + } +} + +// NewUpdateProjectEndpoint returns an endpoint function that calls the method +// "update-project" of service "project-service". +func NewUpdateProjectEndpoint(s Service, authJWTFn security.AuthJWTFunc) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + p := req.(*UpdateProjectPayload) + var err error + sc := security.JWTScheme{ + Name: "jwt", + Scopes: []string{}, + RequiredScopes: []string{}, + } + var token string + if p.BearerToken != nil { + token = *p.BearerToken + } + ctx, err = authJWTFn(ctx, token, &sc) + if err != nil { + return nil, err + } + return s.UpdateProject(ctx, p) + } +} + +// NewDeleteProjectEndpoint returns an endpoint function that calls the method +// "delete-project" of service "project-service". +func NewDeleteProjectEndpoint(s Service, authJWTFn security.AuthJWTFunc) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + p := req.(*DeleteProjectPayload) + var err error + sc := security.JWTScheme{ + Name: "jwt", + Scopes: []string{}, + RequiredScopes: []string{}, + } + var token string + if p.BearerToken != nil { + token = *p.BearerToken + } + ctx, err = authJWTFn(ctx, token, &sc) + if err != nil { + return nil, err + } + return nil, s.DeleteProject(ctx, p) + } +} + +// NewReadyzEndpoint returns an endpoint function that calls the method +// "readyz" of service "project-service". +func NewReadyzEndpoint(s Service) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + return s.Readyz(ctx) + } +} + +// NewLivezEndpoint returns an endpoint function that calls the method "livez" +// of service "project-service". +func NewLivezEndpoint(s Service) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + return s.Livez(ctx) + } +} diff --git a/cmd/project-api/gen/project_service/service.go b/cmd/project-api/gen/project_service/service.go new file mode 100644 index 0000000..106b71b --- /dev/null +++ b/cmd/project-api/gen/project_service/service.go @@ -0,0 +1,296 @@ +// Code generated by goa v3.21.1, DO NOT EDIT. +// +// project-service service +// +// Command: +// $ goa gen +// github.com/linuxfoundation/lfx-v2-project-service/cmd/project-api/design + +package projectservice + +import ( + "context" + + "goa.design/goa/v3/security" +) + +// The project service provides LFX Project resources. +type Service interface { + // Get all projects. + GetProjects(context.Context, *GetProjectsPayload) (res *GetProjectsResult, err error) + // Create a new project. + CreateProject(context.Context, *CreateProjectPayload) (res *Project, err error) + // Get a single project. + GetOneProject(context.Context, *GetOneProjectPayload) (res *GetOneProjectResult, err error) + // Update an existing project. + UpdateProject(context.Context, *UpdateProjectPayload) (res *Project, err error) + // Delete an existing project. + DeleteProject(context.Context, *DeleteProjectPayload) (err error) + // Check if the service is able to take inbound requests. + Readyz(context.Context) (res []byte, err error) + // Check if the service is alive. + Livez(context.Context) (res []byte, err error) +} + +// Auther defines the authorization functions to be implemented by the service. +type Auther interface { + // JWTAuth implements the authorization logic for the JWT security scheme. + JWTAuth(ctx context.Context, token string, schema *security.JWTScheme) (context.Context, error) +} + +// APIName is the name of the API as defined in the design. +const APIName = "project-service" + +// APIVersion is the version of the API as defined in the design. +const APIVersion = "0.0.1" + +// ServiceName is the name of the service as defined in the design. This is the +// same value that is set in the endpoint request contexts under the ServiceKey +// key. +const ServiceName = "project-service" + +// MethodNames lists the service method names as defined in the design. These +// are the same values that are set in the endpoint request contexts under the +// MethodKey key. +var MethodNames = [7]string{"get-projects", "create-project", "get-one-project", "update-project", "delete-project", "readyz", "livez"} + +type BadRequestError struct { + // HTTP status code + Code string + // Error message + Message string +} + +type ConflictError struct { + // HTTP status code + Code string + // Error message + Message string +} + +// CreateProjectPayload is the payload type of the project-service service +// create-project method. +type CreateProjectPayload struct { + // JWT token issued by Heimdall + BearerToken *string + // Version of the API + Version *string + // Project slug, a short slugified name of the project + Slug string + // A description of the project + Description string + // The pretty name of the project + Name string + // Whether the project is public + Public *bool + // The UID of the parent project, should be empty if there is none + ParentUID *string + // A list of project auditors by their user IDs + Auditors []string + // A list of project writers by their user IDs + Writers []string +} + +// DeleteProjectPayload is the payload type of the project-service service +// delete-project method. +type DeleteProjectPayload struct { + // JWT token issued by Heimdall + BearerToken *string + // ETag header value + Etag *string + // Version of the API + Version *string + // Project ID -- v2 id, not related to v1 id directly + ID *string +} + +// GetOneProjectPayload is the payload type of the project-service service +// get-one-project method. +type GetOneProjectPayload struct { + // JWT token issued by Heimdall + BearerToken *string + // Version of the API + Version *string + // Project ID -- v2 id, not related to v1 id directly + ID *string +} + +// GetOneProjectResult is the result type of the project-service service +// get-one-project method. +type GetOneProjectResult struct { + Project *Project + // ETag header value + Etag *string +} + +// GetProjectsPayload is the payload type of the project-service service +// get-projects method. +type GetProjectsPayload struct { + // JWT token issued by Heimdall + BearerToken *string + // Version of the API + Version *string +} + +// GetProjectsResult is the result type of the project-service service +// get-projects method. +type GetProjectsResult struct { + // Resources found + Projects []*Project + // Cache control header + CacheControl *string +} + +type InternalServerError struct { + // HTTP status code + Code string + // Error message + Message string +} + +type NotFoundError struct { + // HTTP status code + Code string + // Error message + Message string +} + +// Project is the result type of the project-service service create-project +// method. +type Project struct { + // Project ID -- v2 id, not related to v1 id directly + ID *string + // Project slug, a short slugified name of the project + Slug *string + // A description of the project + Description *string + // The pretty name of the project + Name *string + // Whether the project is public + Public *bool + // The UID of the parent project, should be empty if there is none + ParentUID *string + // A list of project auditors by their user IDs + Auditors []string + // A list of project writers by their user IDs + Writers []string +} + +type ServiceUnavailableError struct { + // HTTP status code + Code string + // Error message + Message string +} + +// UpdateProjectPayload is the payload type of the project-service service +// update-project method. +type UpdateProjectPayload struct { + // JWT token issued by Heimdall + BearerToken *string + // ETag header value + Etag *string + // Version of the API + Version *string + // Project ID -- v2 id, not related to v1 id directly + ID *string + // Project slug, a short slugified name of the project + Slug string + // A description of the project + Description string + // The pretty name of the project + Name string + // Whether the project is public + Public *bool + // The UID of the parent project, should be empty if there is none + ParentUID *string + // A list of project auditors by their user IDs + Auditors []string + // A list of project writers by their user IDs + Writers []string +} + +// Error returns an error description. +func (e *BadRequestError) Error() string { + return "" +} + +// ErrorName returns "BadRequestError". +// +// Deprecated: Use GoaErrorName - https://github.com/goadesign/goa/issues/3105 +func (e *BadRequestError) ErrorName() string { + return e.GoaErrorName() +} + +// GoaErrorName returns "BadRequestError". +func (e *BadRequestError) GoaErrorName() string { + return "BadRequest" +} + +// Error returns an error description. +func (e *ConflictError) Error() string { + return "" +} + +// ErrorName returns "ConflictError". +// +// Deprecated: Use GoaErrorName - https://github.com/goadesign/goa/issues/3105 +func (e *ConflictError) ErrorName() string { + return e.GoaErrorName() +} + +// GoaErrorName returns "ConflictError". +func (e *ConflictError) GoaErrorName() string { + return "Conflict" +} + +// Error returns an error description. +func (e *InternalServerError) Error() string { + return "" +} + +// ErrorName returns "InternalServerError". +// +// Deprecated: Use GoaErrorName - https://github.com/goadesign/goa/issues/3105 +func (e *InternalServerError) ErrorName() string { + return e.GoaErrorName() +} + +// GoaErrorName returns "InternalServerError". +func (e *InternalServerError) GoaErrorName() string { + return "InternalServerError" +} + +// Error returns an error description. +func (e *NotFoundError) Error() string { + return "" +} + +// ErrorName returns "NotFoundError". +// +// Deprecated: Use GoaErrorName - https://github.com/goadesign/goa/issues/3105 +func (e *NotFoundError) ErrorName() string { + return e.GoaErrorName() +} + +// GoaErrorName returns "NotFoundError". +func (e *NotFoundError) GoaErrorName() string { + return "NotFound" +} + +// Error returns an error description. +func (e *ServiceUnavailableError) Error() string { + return "" +} + +// ErrorName returns "ServiceUnavailableError". +// +// Deprecated: Use GoaErrorName - https://github.com/goadesign/goa/issues/3105 +func (e *ServiceUnavailableError) ErrorName() string { + return e.GoaErrorName() +} + +// GoaErrorName returns "ServiceUnavailableError". +func (e *ServiceUnavailableError) GoaErrorName() string { + return "ServiceUnavailable" +} diff --git a/cmd/project-api/jwt.go b/cmd/project-api/jwt.go new file mode 100644 index 0000000..8beb562 --- /dev/null +++ b/cmd/project-api/jwt.go @@ -0,0 +1,148 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +package main + +import ( + "context" + "errors" + "log/slog" + "net/url" + "os" + "strings" + "time" + + "github.com/auth0/go-jwt-middleware/v2/jwks" + "github.com/auth0/go-jwt-middleware/v2/validator" +) + +const ( + // PS256 is the default for Heimdall's JWT finalizer. + signatureAlgorithm = validator.PS256 + defaultIssuer = "heimdall" + defaultAudience = "lfx-v2-project-service" +) + +var ( + // Factory for custom JWT claims target. + customClaims = func() validator.CustomClaims { + return &HeimdallClaims{} + } +) + +// HeimdallClaims contains extra custom claims we want to parse from the JWT +// token. +type HeimdallClaims struct { + Principal string `json:"principal"` + Email string `json:"email,omitempty"` +} + +// Validate provides additional middleware validation of any claims defined in +// HeimdallClaims. +func (c *HeimdallClaims) Validate(_ context.Context) error { + if c.Principal == "" { + return errors.New("principal must be provided") + } + return nil +} + +type jwtAuth struct { + validator *validator.Validator +} + +func setupJWTAuth() *jwtAuth { + // Set up Heimdall JWKS key provider. + jwksEnv := os.Getenv("JWKS_URL") + if jwksEnv == "" { + jwksEnv = "http://heimdall:4457/.well-known/jwks" + } + jwksURL, err := url.Parse(jwksEnv) + if err != nil { + slog.With(errKey, err).Error("invalid JWKS_URL") + os.Exit(1) + } + var issuer *url.URL + issuer, err = url.Parse(defaultIssuer) + if err != nil { + // This shouldn't happen; a bare hostname is a valid URL. + slog.Error("unexpected URL parsing of default issuer") + os.Exit(1) + } + provider := jwks.NewCachingProvider(issuer, 5*time.Minute, jwks.WithCustomJWKSURI(jwksURL)) + + // Set up the JWT validator. + audience := os.Getenv("AUDIENCE") + if audience == "" { + audience = defaultAudience + } + jwtValidator, err := validator.New( + provider.KeyFunc, + signatureAlgorithm, + issuer.String(), + []string{audience}, + validator.WithCustomClaims(customClaims), + validator.WithAllowedClockSkew(5*time.Second), + ) + if err != nil { + slog.With(errKey, err).Error("failed to set up the Heimdall JWT validator") + os.Exit(1) + } + + return &jwtAuth{ + validator: jwtValidator, + } +} + +// parsePrincipal extracts the principal from the JWT claims. +func (j *jwtAuth) parsePrincipal(ctx context.Context, token string, logger *slog.Logger) (string, error) { + // To avoid having to use a valid JWT token for local development, we can set the + // JWT_AUTH_DISABLED_MOCK_LOCAL_PRINCIPAL environment variable to the principal + // we want to use for local development. + if mockLocalPrincipal := os.Getenv("JWT_AUTH_DISABLED_MOCK_LOCAL_PRINCIPAL"); mockLocalPrincipal != "" { + logger.InfoContext(ctx, "JWT authentication is disabled, returning mock principal", + "principal", mockLocalPrincipal, + ) + return mockLocalPrincipal, nil + } + + if j.validator == nil { + return "", errors.New("JWT validator is not set up") + } + + parsedJWT, err := j.validator.ValidateToken(ctx, token) + if err != nil { + // Drop tertiary (and deeper) nested errors for security reasons. This is + // using colons as an approximation for error nesting, which may not + // exactly match to error boundaries. Unwrapping the error twice, then + // dropping the suffix of the 3rd error's String() method could be more + // accurate to error boundaries, but could also expose tertiary errors if + // errors are not wrapped with Go 1.13 `%w` semantics. + logger.With("default_audience", defaultAudience).With("default_issuer", defaultIssuer).With(errKey, err).WarnContext(ctx, "authorization failed") + errString := err.Error() + firstColon := strings.Index(errString, ":") + if firstColon != -1 && firstColon+1 < len(errString) { + errString = strings.Replace(errString, ": go-jose/go-jose/jwt", "", 1) + secondColon := strings.Index(errString[firstColon+1:], ":") + if secondColon != -1 { + // Error has two colons (which may be 3 or more errors), so drop the + // second colon and everything after it. + errString = errString[:firstColon+secondColon+1] + } + } + return "", errors.New(errString) + } + + claims, ok := parsedJWT.(*validator.ValidatedClaims) + if !ok { + // This should never happen. + return "", errors.New("failed to get validated authorization claims") + } + + customClaims, ok := claims.CustomClaims.(*HeimdallClaims) + if !ok { + // This should never happen. + return "", errors.New("failed to get custom authorization claims") + } + + return customClaims.Principal, nil +} diff --git a/cmd/project-api/main.go b/cmd/project-api/main.go new file mode 100644 index 0000000..cd15e14 --- /dev/null +++ b/cmd/project-api/main.go @@ -0,0 +1,344 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +// Package main is the project service API that provides a RESTful API for managing projects +// and handles NATS messages for the project service. +package main + +import ( + "context" + "embed" + _ "expvar" + "flag" + "fmt" + "log/slog" + "net/http" + "os" + "os/signal" + "sync" + "syscall" + "time" + + nats "github.com/nats-io/nats.go" + "github.com/nats-io/nats.go/jetstream" + goahttp "goa.design/goa/v3/http" + + genhttp "github.com/linuxfoundation/lfx-v2-project-service/cmd/project-api/gen/http/project_service/server" + genquerysvc "github.com/linuxfoundation/lfx-v2-project-service/cmd/project-api/gen/project_service" + "github.com/linuxfoundation/lfx-v2-project-service/internal/log" + "github.com/linuxfoundation/lfx-v2-project-service/internal/middleware" + "github.com/linuxfoundation/lfx-v2-project-service/pkg/constants" +) + +//go:embed gen/http/openapi3.json gen/http/openapi3.yaml +var StaticFS embed.FS + +const ( + // errKey is the key for the error field in the slog. + errKey = "error" + // gracefulShutdownSeconds should be higher than NATS client + // request timeout, and lower than the pod or liveness probe's + // terminationGracePeriodSeconds. + gracefulShutdownSeconds = 25 +) + +func main() { + env := parseEnv() + flags := parseFlags(env.Port) + + log.InitStructureLogConfig() + + // Set up JWT validator needed by the [ProjectsService.JWTAuth] security handler. + jwtAuth := setupJWTAuth() + + // Generated service initialization. + svc := &ProjectsService{ + lfxEnvironment: env.LFXEnvironment, + auth: jwtAuth, + } + + gracefulCloseWG := sync.WaitGroup{} + + httpServer := setupHTTPServer(flags, svc, &gracefulCloseWG) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + done := make(chan os.Signal, 1) + signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + natsConn, err := setupNATS(ctx, env, svc, &gracefulCloseWG, done) + if err != nil { + slog.With(errKey, err).Error("error setting up NATS") + return + } + + // This next line blocks until SIGINT or SIGTERM is received. + <-done + + gracefulShutdown(httpServer, natsConn, &gracefulCloseWG, cancel) + +} + +// flags are the command line flags for the project service. +type flags struct { + Debug bool + Port string + Bind string +} + +func parseFlags(defaultPort string) flags { + var debug = flag.Bool("d", false, "enable debug logging") + var port = flag.String("p", defaultPort, "listen port") + var bind = flag.String("bind", "*", "interface to bind on") + + flag.Usage = func() { + flag.PrintDefaults() + os.Exit(2) + } + flag.Parse() + + // Based on the debug flag, set the log level environment variable used by [log.InitStructureLogConfig] + if *debug { + err := os.Setenv("LOG_LEVEL", "debug") + if err != nil { + slog.With(errKey, err).Error("error setting log level") + os.Exit(1) + } + } + + return flags{ + Debug: *debug, + Port: *port, + Bind: *bind, + } +} + +// environment are the environment variables for the project service. +type environment struct { + LFXEnvironment constants.LFXEnvironment + NatsURL string + Port string +} + +func parseEnv() environment { + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + natsURL := os.Getenv("NATS_URL") + if natsURL == "" { + natsURL = "nats://localhost:4222" + } + lfxEnvironment := constants.ParseLFXEnvironment(os.Getenv("LFX_ENVIRONMENT")) + return environment{ + LFXEnvironment: lfxEnvironment, + NatsURL: natsURL, + Port: port, + } +} + +func setupHTTPServer(flags flags, svc *ProjectsService, gracefulCloseWG *sync.WaitGroup) *http.Server { + // Wrap it in the generated endpoints + endpoints := genquerysvc.NewEndpoints(svc) + + // Build an HTTP handler + mux := goahttp.NewMuxer() + requestDecoder := goahttp.RequestDecoder + responseEncoder := goahttp.ResponseEncoder + + // Create a custom encoder that sets ETag header for get-one-project + customEncoder := func(ctx context.Context, w http.ResponseWriter) goahttp.Encoder { + encoder := responseEncoder(ctx, w) + + // Check if we have an ETag in the context + if etag, ok := ctx.Value(constants.ETagContextID).(string); ok { + w.Header().Set("ETag", etag) + } + + return encoder + } + + genHttpServer := genhttp.New( + endpoints, + mux, + requestDecoder, + customEncoder, + nil, + nil, + http.FS(StaticFS)) + + // Mount the handler on the mux + genhttp.Mount(mux, genHttpServer) + + var handler http.Handler = mux + + // Add HTTP middleware + // Note: Order matters - RequestIDMiddleware should come first in the chain, + // so it should be the last middleware added to the handler since it is executed in reverse order. + handler = middleware.RequestLoggerMiddleware()(handler) + handler = middleware.RequestIDMiddleware()(handler) + handler = middleware.AuthorizationMiddleware()(handler) + + // Set up http listener in a goroutine using provided command line parameters. + var addr string + if flags.Bind == "*" { + addr = ":" + flags.Port + } else { + addr = flags.Bind + ":" + flags.Port + } + httpServer := &http.Server{ + Addr: addr, + Handler: handler, + ReadHeaderTimeout: 3 * time.Second, + } + gracefulCloseWG.Add(1) + go func() { + slog.With("addr", addr).Debug("starting http server, listening on port " + flags.Port) + err := httpServer.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + slog.With(errKey, err).Error("http listener error") + os.Exit(1) + } + // Because ErrServerClosed is *immediately* returned when Shutdown is + // called, not when when Shutdown completes, this must not yet decrement + // the wait group. + }() + + return httpServer +} + +func setupNATS(ctx context.Context, env environment, svc *ProjectsService, gracefulCloseWG *sync.WaitGroup, done chan os.Signal) (*nats.Conn, error) { + // Create NATS connection. + gracefulCloseWG.Add(1) + var err error + slog.With("nats_url", env.NatsURL).Info("attempting to connect to NATS") + natsConn, err := nats.Connect( + env.NatsURL, + nats.DrainTimeout(gracefulShutdownSeconds*time.Second), + nats.ConnectHandler(func(_ *nats.Conn) { + slog.With("nats_url", env.NatsURL).Info("NATS connection established") + }), + nats.ErrorHandler(func(_ *nats.Conn, s *nats.Subscription, err error) { + if s != nil { + slog.With(errKey, err, "subject", s.Subject, "queue", s.Queue).Error("async NATS error") + } else { + slog.With(errKey, err).Error("async NATS error outside subscription") + } + }), + nats.ClosedHandler(func(_ *nats.Conn) { + if ctx.Err() != nil { + // If our parent background context has already been canceled, this is + // a graceful shutdown. Decrement the wait group but do not exit, to + // allow other graceful shutdown steps to complete. + slog.With("nats_url", env.NatsURL).Info("NATS connection closed gracefully") + gracefulCloseWG.Done() + return + } + // Otherwise, this handler means that max reconnect attempts have been + // exhausted. + slog.With("nats_url", env.NatsURL).Error("NATS max-reconnects exhausted; connection closed") + // Send a synthetic interrupt and give any graceful-shutdown tasks 5 + // seconds to clean up. + done <- os.Interrupt + time.Sleep(5 * time.Second) + // Exit with an error instead of decrementing the wait group. + os.Exit(1) + }), + ) + if err != nil { + slog.With("nats_url", env.NatsURL, errKey, err).Error("error creating NATS client") + return nil, err + } + svc.natsConn = natsConn + + // Get the key-value store for projects. + svc.projectsKV, err = getKeyValueStore(ctx, natsConn) + if err != nil { + return natsConn, err + } + + // Create NATS subscriptions for the project service. + err = createNatsSubcriptions(ctx, svc, natsConn) + if err != nil { + return natsConn, err + } + + return natsConn, nil +} + +// getKeyValueStore creates a JetStream client and gets the key-value store for projects. +func getKeyValueStore(ctx context.Context, natsConn *nats.Conn) (jetstream.KeyValue, error) { + js, err := jetstream.New(natsConn) + if err != nil { + slog.ErrorContext(ctx, "error creating NATS JetStream client", "nats_url", natsConn.ConnectedUrl(), errKey, err) + return nil, err + } + projectsKV, err := js.KeyValue(ctx, constants.KVBucketNameProjects) + if err != nil { + slog.ErrorContext(ctx, "error getting NATS JetStream key-value store", "nats_url", natsConn.ConnectedUrl(), errKey, err, "bucket", constants.KVBucketNameProjects) + return nil, err + } + return projectsKV, nil +} + +// createNatsSubcriptions creates the NATS subscriptions for the project service. +func createNatsSubcriptions(ctx context.Context, svc *ProjectsService, natsConn *nats.Conn) error { + slog.InfoContext(ctx, "subscribing to NATS subjects", "nats_url", natsConn.ConnectedUrl(), "servers", natsConn.Servers(), "subjects", []string{constants.ProjectGetNameSubject, constants.ProjectSlugToUIDSubject}) + queueName := fmt.Sprintf("%s%s", svc.lfxEnvironment, constants.ProjectsAPIQueue) + + // Get project name subscription + projectGetNameSubject := fmt.Sprintf("%s%s", svc.lfxEnvironment, constants.ProjectGetNameSubject) + _, err := natsConn.QueueSubscribe(projectGetNameSubject, queueName, func(msg *nats.Msg) { + svc.HandleNatsMessage(&NatsMsg{msg}) + }) + if err != nil { + slog.ErrorContext(ctx, "error creating NATS queue subscription", errKey, err) + return err + } + + // Get project slug to UID subscription + projectSlugToUIDSubject := fmt.Sprintf("%s%s", svc.lfxEnvironment, constants.ProjectSlugToUIDSubject) + _, err = natsConn.QueueSubscribe(projectSlugToUIDSubject, queueName, func(msg *nats.Msg) { + svc.HandleNatsMessage(&NatsMsg{msg}) + }) + if err != nil { + slog.ErrorContext(ctx, "error creating NATS queue subscription", errKey, err) + return err + } + + return nil +} + +func gracefulShutdown(httpServer *http.Server, natsConn *nats.Conn, gracefulCloseWG *sync.WaitGroup, cancel context.CancelFunc) { + // Cancel the background context. + cancel() + + go func() { + // Run the HTTP shutdown in a goroutine so the NATS draining can also start. + ctx, cancel := context.WithTimeout(context.Background(), gracefulShutdownSeconds*time.Second) + defer cancel() + + slog.With("addr", httpServer.Addr).Info("shutting down http server") + if err := httpServer.Shutdown(ctx); err != nil { + slog.With(errKey, err).Error("http shutdown error") + } + // Decrement the wait group. + gracefulCloseWG.Done() + }() + + // Drain the NATS connection, which will drain all subscriptions, then close the + // connection when complete. + if !natsConn.IsClosed() && !natsConn.IsDraining() { + slog.Info("draining NATS connections") + if err := natsConn.Drain(); err != nil { + slog.With(errKey, err).Error("error draining NATS connection") + // Skip waiting or checking error channel. + return + } + } + + // Wait for the HTTP graceful shutdown and for the NATS connection to be + // closed (see nats.Connect options for the timeout and the handler that + // decrements the wait group). + gracefulCloseWG.Wait() +} diff --git a/cmd/project-api/mock.go b/cmd/project-api/mock.go new file mode 100644 index 0000000..f917feb --- /dev/null +++ b/cmd/project-api/mock.go @@ -0,0 +1,22 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +package main + +import ( + "context" + "log/slog" + + "github.com/stretchr/testify/mock" +) + +// MockJwtAuth is a mock implementation of the [IJwtAuth] interface. +type MockJwtAuth struct { + mock.Mock +} + +// parsePrincipal is a mock method for the [IJwtAuth] interface. +func (m *MockJwtAuth) parsePrincipal(ctx context.Context, token string, logger *slog.Logger) (string, error) { + args := m.Called(ctx, token, logger) + return args.String(0), args.Error(1) +} diff --git a/cmd/project-api/repo.go b/cmd/project-api/repo.go new file mode 100644 index 0000000..3e37e7c --- /dev/null +++ b/cmd/project-api/repo.go @@ -0,0 +1,60 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +package main + +import ( + "time" + + projsvc "github.com/linuxfoundation/lfx-v2-project-service/cmd/project-api/gen/project_service" + "github.com/linuxfoundation/lfx-v2-project-service/internal/infrastructure/nats" +) + +// ConvertToDBProject converts a project service project to a project database representation. +func ConvertToDBProject(project *projsvc.Project) *nats.ProjectDB { + if project == nil { + return new(nats.ProjectDB) + } + + currentTime := time.Now() + + p := new(nats.ProjectDB) + if project.ID != nil { + p.UID = *project.ID + } + if project.Slug != nil { + p.Slug = *project.Slug + } + if project.Name != nil { + p.Name = *project.Name + } + if project.Description != nil { + p.Description = *project.Description + } + if project.Public != nil { + p.Public = *project.Public + } + if project.ParentUID != nil { + p.ParentUID = *project.ParentUID + } + p.Auditors = project.Auditors + p.Writers = project.Writers + p.CreatedAt = currentTime + p.UpdatedAt = currentTime + + return p +} + +// ConvertToServiceProject converts a project database representation to a project service project. +func ConvertToServiceProject(p *nats.ProjectDB) *projsvc.Project { + return &projsvc.Project{ + ID: &p.UID, + Slug: &p.Slug, + Name: &p.Name, + Description: &p.Description, + Public: &p.Public, + ParentUID: &p.ParentUID, + Auditors: p.Auditors, + Writers: p.Writers, + } +} diff --git a/cmd/project-api/service.go b/cmd/project-api/service.go new file mode 100644 index 0000000..79a1f48 --- /dev/null +++ b/cmd/project-api/service.go @@ -0,0 +1,25 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +package main + +import ( + "context" + "log/slog" + + "github.com/linuxfoundation/lfx-v2-project-service/internal/infrastructure/nats" + "github.com/linuxfoundation/lfx-v2-project-service/pkg/constants" +) + +// ProjectsService implements the projsvc.Service interface +type ProjectsService struct { + lfxEnvironment constants.LFXEnvironment + projectsKV nats.INatsKeyValue + natsConn nats.INatsConn + auth IJwtAuth +} + +// IJwtAuth is a JWT authentication interface needed for the [ProjectsService]. +type IJwtAuth interface { + parsePrincipal(ctx context.Context, token string, logger *slog.Logger) (string, error) +} diff --git a/cmd/project-api/service_endpoint.go b/cmd/project-api/service_endpoint.go new file mode 100644 index 0000000..fdad7fc --- /dev/null +++ b/cmd/project-api/service_endpoint.go @@ -0,0 +1,79 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +package main + +import ( + "context" + "log/slog" + "net/http" + "strconv" + + projsvc "github.com/linuxfoundation/lfx-v2-project-service/cmd/project-api/gen/project_service" + "github.com/linuxfoundation/lfx-v2-project-service/pkg/constants" + "goa.design/goa/v3/security" +) + +// createResponse creates a response error based on the HTTP status code. +func createResponse(code int, message string) error { + switch code { + case http.StatusBadRequest: + return &projsvc.BadRequestError{ + Code: strconv.Itoa(code), + Message: message, + } + case http.StatusNotFound: + return &projsvc.NotFoundError{ + Code: strconv.Itoa(code), + Message: message, + } + case http.StatusConflict: + return &projsvc.ConflictError{ + Code: strconv.Itoa(code), + Message: message, + } + case http.StatusInternalServerError: + return &projsvc.InternalServerError{ + Code: strconv.Itoa(code), + Message: message, + } + case http.StatusServiceUnavailable: + return &projsvc.ServiceUnavailableError{ + Code: strconv.Itoa(code), + Message: message, + } + default: + return nil + } +} + +// Readyz checks if the service is able to take inbound requests. +func (s *ProjectsService) Readyz(_ context.Context) ([]byte, error) { + if s.natsConn == nil || s.projectsKV == nil { + return nil, createResponse(http.StatusServiceUnavailable, "service unavailable") + } + if !s.natsConn.IsConnected() { + return nil, createResponse(http.StatusServiceUnavailable, "NATS connection not established") + } + return []byte("OK\n"), nil +} + +// Livez checks if the service is alive. +func (s *ProjectsService) Livez(_ context.Context) ([]byte, error) { + // This always returns as long as the service is still running. As this + // endpoint is expected to be used as a Kubernetes liveness check, this + // service must likewise self-detect non-recoverable errors and + // self-terminate. + return []byte("OK\n"), nil +} + +// JWTAuth implements Auther interface for the JWT security scheme. +func (s *ProjectsService) JWTAuth(ctx context.Context, bearerToken string, _ *security.JWTScheme) (context.Context, error) { + // Parse the Heimdall-authorized principal from the token. + principal, err := s.auth.parsePrincipal(ctx, bearerToken, slog.Default()) + if err != nil { + return ctx, err + } + // Return a new context containing the principal as a value. + return context.WithValue(ctx, constants.PrincipalContextID, principal), nil +} diff --git a/cmd/project-api/service_endpoint_project.go b/cmd/project-api/service_endpoint_project.go new file mode 100644 index 0000000..d8ddbd5 --- /dev/null +++ b/cmd/project-api/service_endpoint_project.go @@ -0,0 +1,363 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +package main + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "log/slog" + "net/http" + "strconv" + "strings" + + "github.com/google/uuid" + projsvc "github.com/linuxfoundation/lfx-v2-project-service/cmd/project-api/gen/project_service" + "github.com/linuxfoundation/lfx-v2-project-service/internal/infrastructure/nats" + "github.com/linuxfoundation/lfx-v2-project-service/internal/log" + "github.com/linuxfoundation/lfx-v2-project-service/pkg/constants" + "github.com/nats-io/nats.go/jetstream" + "golang.org/x/sync/errgroup" +) + +// GetProjects fetches all projects +func (s *ProjectsService) GetProjects(ctx context.Context, payload *projsvc.GetProjectsPayload) (*projsvc.GetProjectsResult, error) { + if s.natsConn == nil || s.projectsKV == nil { + slog.ErrorContext(ctx, "NATS connection or KeyValue store not initialized") + return nil, createResponse(http.StatusServiceUnavailable, "service unavailable") + } + + keysLister, err := s.projectsKV.ListKeys(ctx) + if err != nil { + slog.ErrorContext(ctx, "error listing project keys from NATS KV store", errKey, err) + return nil, createResponse(http.StatusInternalServerError, "error listing project keys from NATS KV store") + } + + projects := []*projsvc.Project{} + for key := range keysLister.Keys() { + if strings.HasPrefix(key, "slug/") { + continue + } + + entry, err := s.projectsKV.Get(ctx, key) + if err != nil { + slog.ErrorContext(ctx, "error getting project from NATS KV store", errKey, err, "project_id", key) + return nil, createResponse(http.StatusInternalServerError, "error getting project from NATS KV store") + } + + projectDB := nats.ProjectDB{} + err = json.Unmarshal(entry.Value(), &projectDB) + if err != nil { + slog.ErrorContext(ctx, "error unmarshalling project from NATS KV store", errKey, err, "project_id", key) + return nil, createResponse(http.StatusInternalServerError, "error unmarshalling project from NATS KV store") + } + + projects = append(projects, ConvertToServiceProject(&projectDB)) + + } + + slog.DebugContext(ctx, "returning projects", "projects", projects) + + return &projsvc.GetProjectsResult{ + Projects: projects, + CacheControl: nil, + }, nil + +} + +// Create a new project. +func (s *ProjectsService) CreateProject(ctx context.Context, payload *projsvc.CreateProjectPayload) (*projsvc.Project, error) { + id := uuid.NewString() // TODO: what type of ID are we using for the project resource? + project := &projsvc.Project{ + ID: &id, + Slug: &payload.Slug, + Description: &payload.Description, + Name: &payload.Name, + Public: payload.Public, + ParentUID: payload.ParentUID, + Auditors: payload.Auditors, + Writers: payload.Writers, + } + + if s.natsConn == nil || s.projectsKV == nil { + slog.ErrorContext(ctx, "NATS connection or KV store not initialized") + return nil, createResponse(http.StatusServiceUnavailable, "service unavailable") + } + + // Validate that the parent UID is a valid UUID and is an existing project UID. + if project.ParentUID != nil && *project.ParentUID != "" { + if _, err := uuid.Parse(*project.ParentUID); err != nil { + slog.ErrorContext(ctx, "invalid parent UID", errKey, err) + return nil, createResponse(http.StatusBadRequest, "invalid parent UID") + } + if _, err := s.projectsKV.Get(ctx, *project.ParentUID); err != nil { + if errors.Is(err, jetstream.ErrKeyNotFound) { + slog.ErrorContext(ctx, "parent project not found", errKey, err) + return nil, createResponse(http.StatusBadRequest, "parent project not found") + } + slog.ErrorContext(ctx, "error getting parent project from NATS KV store", errKey, err) + return nil, createResponse(http.StatusInternalServerError, "error getting parent project from NATS KV store") + } + + } + + projectDB := ConvertToDBProject(project) + slog.With("project_id", projectDB.UID, "project_slug", projectDB.Slug) + _, err := s.projectsKV.Put(ctx, fmt.Sprintf("slug/%s", projectDB.Slug), []byte(projectDB.UID)) + if err != nil { + if errors.Is(err, jetstream.ErrKeyExists) { + slog.WarnContext(ctx, "project already exists", errKey, err) + return nil, createResponse(http.StatusConflict, "project already exists") + } + slog.ErrorContext(ctx, "error putting project UID mapping into NATS KV store", errKey, err) + return nil, createResponse(http.StatusInternalServerError, "error putting project UID mapping into NATS KV store") + } + + projectDBBytes, err := json.Marshal(projectDB) + if err != nil { + slog.ErrorContext(ctx, "error marshalling project into JSON", errKey, err) + return nil, createResponse(http.StatusInternalServerError, "error marshalling project into JSON") + } + _, err = s.projectsKV.Put(ctx, projectDB.UID, projectDBBytes) + if err != nil { + slog.ErrorContext(ctx, "error putting project into NATS KV store", errKey, err) + return nil, createResponse(http.StatusInternalServerError, "error putting project into NATS KV store") + } + + messageBuilder := nats.MessageBuilder{ + NatsConn: s.natsConn, + LfxEnvironment: s.lfxEnvironment, + } + + g := new(errgroup.Group) + g.Go(func() error { + return messageBuilder.SendIndexProjectTransaction(ctx, nats.ActionCreated, projectDBBytes) + }) + + g.Go(func() error { + return messageBuilder.SendUpdateAccessProjectTransaction(ctx, projectDBBytes) + }) + + if err := g.Wait(); err != nil { + // Return the first error from the goroutines. + return nil, createResponse(http.StatusInternalServerError, fmt.Sprintf("error sending transactions to NATS: %s", err.Error())) + } + + slog.DebugContext(ctx, "returning created project", "project", project) + + return project, nil +} + +// Get a single project. +func (s *ProjectsService) GetOneProject(ctx context.Context, payload *projsvc.GetOneProjectPayload) (*projsvc.GetOneProjectResult, error) { + if payload == nil || payload.ID == nil { + slog.WarnContext(ctx, "project ID is required") + return nil, createResponse(http.StatusBadRequest, "project ID is required") + } + + ctx = log.AppendCtx(ctx, slog.String("project_id", *payload.ID)) + if s.natsConn == nil || s.projectsKV == nil { + slog.ErrorContext(ctx, "NATS connection or KV store not initialized") + return nil, createResponse(http.StatusServiceUnavailable, "service unavailable") + } + + entry, err := s.projectsKV.Get(ctx, *payload.ID) + if err != nil { + if errors.Is(err, jetstream.ErrKeyNotFound) { + slog.WarnContext(ctx, "project not found", errKey, err) + return nil, createResponse(http.StatusNotFound, "project not found") + } + slog.ErrorContext(ctx, "error getting project from NATS KV store", errKey, err) + return nil, createResponse(http.StatusInternalServerError, "error getting project from NATS KV store") + } + + projectDB := nats.ProjectDB{} + err = json.Unmarshal(entry.Value(), &projectDB) + if err != nil { + slog.ErrorContext(ctx, "error unmarshalling project from NATS KV store", errKey, err) + return nil, createResponse(http.StatusInternalServerError, "error unmarshalling project from NATS KV store") + } + project := ConvertToServiceProject(&projectDB) + + // Store the revision in context for the custom encoder to use + revision := entry.Revision() + revisionStr := strconv.FormatUint(revision, 10) + ctx = context.WithValue(ctx, constants.ETagContextID, revisionStr) + + slog.DebugContext(ctx, "returning project", "project", project, "revision", revision) + + return &projsvc.GetOneProjectResult{ + Project: project, + Etag: &revisionStr, + }, nil +} + +// Update a project. +func (s *ProjectsService) UpdateProject(ctx context.Context, payload *projsvc.UpdateProjectPayload) (*projsvc.Project, error) { + if payload == nil || payload.ID == nil { + slog.WarnContext(ctx, "project ID is required") + return nil, createResponse(http.StatusBadRequest, "project ID is required") + } + if payload.Etag == nil { + slog.WarnContext(ctx, "ETag header is missing") + return nil, createResponse(http.StatusBadRequest, "ETag header is missing") + } + revision, err := strconv.ParseUint(*payload.Etag, 10, 64) + if err != nil { + slog.ErrorContext(ctx, "error parsing ETag", errKey, err) + return nil, createResponse(http.StatusBadRequest, "error parsing ETag header") + } + ctx = log.AppendCtx(ctx, slog.String("project_id", *payload.ID)) + + if s.natsConn == nil || s.projectsKV == nil { + slog.ErrorContext(ctx, "NATS connection or KV store not initialized") + return nil, createResponse(http.StatusServiceUnavailable, "service unavailable") + } + + // Validate that the parent UID is a valid UUID and is an existing project UID. + if payload.ParentUID != nil && *payload.ParentUID != "" { + if _, err := uuid.Parse(*payload.ParentUID); err != nil { + slog.ErrorContext(ctx, "invalid parent UID", errKey, err) + return nil, createResponse(http.StatusBadRequest, "invalid parent UID") + } + if _, err := s.projectsKV.Get(ctx, *payload.ParentUID); err != nil { + if errors.Is(err, jetstream.ErrKeyNotFound) { + slog.ErrorContext(ctx, "parent project not found", errKey, err) + return nil, createResponse(http.StatusBadRequest, "parent project not found") + } + slog.ErrorContext(ctx, "error getting parent project from NATS KV store", errKey, err) + return nil, createResponse(http.StatusInternalServerError, "error getting parent project from NATS KV store") + } + + } + + // Check if the project exists + _, err = s.projectsKV.Get(ctx, *payload.ID) + if err != nil { + if errors.Is(err, jetstream.ErrKeyNotFound) { + slog.WarnContext(ctx, "project not found", errKey, err) + return nil, createResponse(http.StatusNotFound, "project not found") + } + slog.ErrorContext(ctx, "error getting project from NATS KV store", errKey, err) + return nil, createResponse(http.StatusInternalServerError, "error getting project from NATS KV store") + } + + // Update the project in the NATS KV store + project := &projsvc.Project{ + ID: payload.ID, + Slug: &payload.Slug, + Description: &payload.Description, + Name: &payload.Name, + Public: payload.Public, + ParentUID: payload.ParentUID, + Auditors: payload.Auditors, + Writers: payload.Writers, + } + projectDB := ConvertToDBProject(project) + projectDBBytes, err := json.Marshal(projectDB) + if err != nil { + slog.ErrorContext(ctx, "error marshalling project into JSON", errKey, err) + return nil, createResponse(http.StatusInternalServerError, "error marshalling project into JSON") + } + _, err = s.projectsKV.Update(ctx, *payload.ID, projectDBBytes, revision) + if err != nil { + if strings.Contains(err.Error(), "wrong last sequence") { + slog.WarnContext(ctx, "etag header is invalid", errKey, err) + return nil, createResponse(http.StatusBadRequest, "etag header is invalid") + } + slog.ErrorContext(ctx, "error updating project in NATS KV store", errKey, err) + return nil, createResponse(http.StatusInternalServerError, "error updating project in NATS KV store") + } + + messageBuilder := nats.MessageBuilder{ + NatsConn: s.natsConn, + LfxEnvironment: s.lfxEnvironment, + } + + g := new(errgroup.Group) + g.Go(func() error { + return messageBuilder.SendIndexProjectTransaction(ctx, nats.ActionUpdated, projectDBBytes) + }) + + g.Go(func() error { + return messageBuilder.SendUpdateAccessProjectTransaction(ctx, projectDBBytes) + }) + + if err := g.Wait(); err != nil { + // Return the first error from the goroutines. + return nil, createResponse(http.StatusInternalServerError, fmt.Sprintf("error sending transactions to NATS: %s", err.Error())) + } + + slog.DebugContext(ctx, "returning updated project", "project", project) + + return project, nil +} + +// Delete a project. +func (s *ProjectsService) DeleteProject(ctx context.Context, payload *projsvc.DeleteProjectPayload) error { + if payload == nil || payload.ID == nil { + slog.WarnContext(ctx, "project ID is required") + return createResponse(http.StatusBadRequest, "project ID is required") + } + if payload.Etag == nil { + slog.WarnContext(ctx, "ETag header is missing") + return createResponse(http.StatusBadRequest, "ETag header is missing") + } + revision, err := strconv.ParseUint(*payload.Etag, 10, 64) + if err != nil { + slog.ErrorContext(ctx, "error parsing ETag", errKey, err) + return createResponse(http.StatusBadRequest, "error parsing ETag header") + } + + ctx = log.AppendCtx(ctx, slog.String("project_id", *payload.ID)) + ctx = log.AppendCtx(ctx, slog.String("etag", strconv.FormatUint(revision, 10))) + + if s.natsConn == nil || s.projectsKV == nil { + slog.ErrorContext(ctx, "NATS connection or KV store not initialized") + return createResponse(http.StatusServiceUnavailable, "service unavailable") + } + + // Check if the project exists + _, err = s.projectsKV.Get(ctx, *payload.ID) + if err != nil { + if errors.Is(err, jetstream.ErrKeyNotFound) { + slog.WarnContext(ctx, "project not found", errKey, err) + return createResponse(http.StatusNotFound, "project not found") + } + } + + // Delete the project from the NATS KV store + err = s.projectsKV.Delete(ctx, *payload.ID, jetstream.LastRevision(revision)) + if err != nil { + if strings.Contains(err.Error(), "wrong last sequence") { + slog.WarnContext(ctx, "etag header is invalid", errKey, err) + return createResponse(http.StatusBadRequest, "etag header is invalid") + } + slog.ErrorContext(ctx, "error deleting project from NATS KV store", errKey, err) + return createResponse(http.StatusInternalServerError, "error deleting project from NATS KV store") + } + + messageBuilder := nats.MessageBuilder{ + NatsConn: s.natsConn, + LfxEnvironment: s.lfxEnvironment, + } + + g := new(errgroup.Group) + g.Go(func() error { + return messageBuilder.SendIndexProjectTransaction(ctx, nats.ActionDeleted, []byte(*payload.ID)) + }) + + g.Go(func() error { + return messageBuilder.SendDeleteAllAccessProjectTransaction(ctx, []byte(*payload.ID)) + }) + + if err := g.Wait(); err != nil { + // Return the first error from the goroutines. + return createResponse(http.StatusInternalServerError, fmt.Sprintf("error sending transactions to NATS: %s", err.Error())) + } + + slog.DebugContext(ctx, "deleted project", "project_id", *payload.ID) + return nil +} diff --git a/cmd/project-api/service_endpoint_project_test.go b/cmd/project-api/service_endpoint_project_test.go new file mode 100644 index 0000000..40ab64e --- /dev/null +++ b/cmd/project-api/service_endpoint_project_test.go @@ -0,0 +1,528 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +package main + +import ( + "context" + "fmt" + "log/slog" + "os" + "testing" + + "github.com/nats-io/nats.go/jetstream" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + projsvc "github.com/linuxfoundation/lfx-v2-project-service/cmd/project-api/gen/project_service" + "github.com/linuxfoundation/lfx-v2-project-service/internal/infrastructure/nats" + "github.com/linuxfoundation/lfx-v2-project-service/pkg/constants" +) + +// setupService creates a new ProjectsService with mocked external service APIs. +func setupService() *ProjectsService { + if os.Getenv("DEBUG") == "true" { + slog.SetLogLoggerLevel(slog.LevelDebug) + } + service := &ProjectsService{ + lfxEnvironment: constants.LFXEnvironmentDev, + natsConn: &nats.MockNATSConn{}, + projectsKV: &nats.MockKeyValue{}, + auth: &MockJwtAuth{}, + } + + return service +} + +func TestGetProjects(t *testing.T) { + tests := []struct { + name string + payload *projsvc.GetProjectsPayload + setupMocks func(*nats.MockKeyValue) + expectedError bool + expectedResult *projsvc.GetProjectsResult + }{ + { + name: "success with projects", + payload: &projsvc.GetProjectsPayload{}, + setupMocks: func(mockKV *nats.MockKeyValue) { + // Create mock key lister with project keys + mockLister := nats.NewMockKeyLister([]string{"project-1", "project-2"}) + mockKV.On("ListKeys", mock.Anything).Return(mockLister, nil) + + // Mock project entries + project1Data := `{"uid":"project-1","slug":"test-1","name":"Test Project 1","description":"Test 1","public":true,"parent_uid":"","auditors":["user1"],"writers":["user2"],"created_at":"2023-01-01T00:00:00Z","updated_at":"2023-01-01T00:00:00Z"}` + project2Data := `{"uid":"project-2","slug":"test-2","name":"Test Project 2","description":"Test 2","public":false,"parent_uid":"parent-uid","auditors":["user2"],"writers":["user3"],"created_at":"2023-01-01T00:00:00Z","updated_at":"2023-01-01T00:00:00Z"}` + + mockKV.On("Get", mock.Anything, "project-1").Return(nats.NewMockKeyValueEntry([]byte(project1Data), 123), nil) + mockKV.On("Get", mock.Anything, "project-2").Return(nats.NewMockKeyValueEntry([]byte(project2Data), 123), nil) + }, + expectedError: false, + expectedResult: &projsvc.GetProjectsResult{ + Projects: []*projsvc.Project{ + { + ID: stringPtr("project-1"), + Slug: stringPtr("test-1"), + Name: stringPtr("Test Project 1"), + Description: stringPtr("Test 1"), + Public: boolPtr(true), + ParentUID: stringPtr(""), + Auditors: []string{"user1"}, + Writers: []string{"user2"}, + }, + { + ID: stringPtr("project-2"), + Slug: stringPtr("test-2"), + Name: stringPtr("Test Project 2"), + Description: stringPtr("Test 2"), + Public: boolPtr(false), + ParentUID: stringPtr("parent-uid"), + Auditors: []string{"user2"}, + Writers: []string{"user3"}, + }, + }, + }, + }, + { + name: "success with no projects", + payload: &projsvc.GetProjectsPayload{}, + setupMocks: func(mockKV *nats.MockKeyValue) { + mockLister := nats.NewMockKeyLister([]string{}) + mockKV.On("ListKeys", mock.Anything).Return(mockLister, nil) + }, + expectedError: false, + expectedResult: &projsvc.GetProjectsResult{ + Projects: []*projsvc.Project{}, + }, + }, + { + name: "error listing keys", + payload: &projsvc.GetProjectsPayload{}, + setupMocks: func(mockKV *nats.MockKeyValue) { + mockKV.On("ListKeys", mock.Anything).Return(&nats.MockKeyLister{}, assert.AnError) + }, + expectedError: true, + }, + { + name: "error getting project", + payload: &projsvc.GetProjectsPayload{}, + setupMocks: func(mockKV *nats.MockKeyValue) { + mockLister := nats.NewMockKeyLister([]string{"project-1"}) + mockKV.On("ListKeys", mock.Anything).Return(mockLister, nil) + mockKV.On("Get", mock.Anything, "project-1").Return(&nats.MockKeyValueEntry{}, assert.AnError) + }, + expectedError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + service := setupService() + tt.setupMocks(service.projectsKV.(*nats.MockKeyValue)) + + result, err := service.GetProjects(context.Background(), tt.payload) + + if tt.expectedError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, len(tt.expectedResult.Projects), len(result.Projects)) + + // Compare project details + for i, expectedProject := range tt.expectedResult.Projects { + if i < len(result.Projects) { + actualProject := result.Projects[i] + assert.Equal(t, *expectedProject.ID, *actualProject.ID) + assert.Equal(t, *expectedProject.Slug, *actualProject.Slug) + assert.Equal(t, *expectedProject.Name, *actualProject.Name) + if expectedProject.Description != nil { + assert.Equal(t, *expectedProject.Description, *actualProject.Description) + } + assert.Equal(t, expectedProject.Public, actualProject.Public) + assert.Equal(t, expectedProject.ParentUID, actualProject.ParentUID) + assert.Equal(t, expectedProject.Auditors, actualProject.Auditors) + assert.Equal(t, expectedProject.Writers, actualProject.Writers) + } + } + } + }) + } +} + +func TestCreateProject(t *testing.T) { + tests := []struct { + name string + payload *projsvc.CreateProjectPayload + setupMocks func(*ProjectsService) + expectedError bool + }{ + { + name: "success", + payload: &projsvc.CreateProjectPayload{ + Slug: "test-project", + Name: "Test Project", + Description: "Test description", + Public: boolPtr(true), + ParentUID: stringPtr(""), + Auditors: []string{"user1", "user2"}, + Writers: []string{"user3", "user4"}, + }, + setupMocks: func(service *ProjectsService) { + mockNats := service.natsConn.(*nats.MockNATSConn) + mockKV := service.projectsKV.(*nats.MockKeyValue) + // Mock successful slug mapping creation + mockKV.On("Put", mock.Anything, "slug/test-project", mock.Anything).Return(uint64(1), nil) + // Mock successful project creation + mockKV.On("Put", mock.Anything, mock.Anything, mock.Anything).Return(uint64(1), nil) + mockNats.On("Publish", fmt.Sprintf("%s%s", service.lfxEnvironment, constants.IndexProjectSubject), mock.Anything).Return(nil) + mockNats.On("Publish", fmt.Sprintf("%s%s", service.lfxEnvironment, constants.UpdateAccessProjectSubject), mock.Anything).Return(nil) + }, + expectedError: false, + }, + { + name: "invalid parent UID", + payload: &projsvc.CreateProjectPayload{ + Slug: "test-project", + Name: "Test Project", + Description: "Test description", + Public: boolPtr(true), + ParentUID: stringPtr("invalid-parent-uid"), + Auditors: []string{"user1"}, + Writers: []string{"user2"}, + }, + setupMocks: func(_ *ProjectsService) {}, + expectedError: true, + }, + { + name: "parent project not found", + payload: &projsvc.CreateProjectPayload{ + Slug: "test-project", + Name: "Test Project", + Description: "Test description", + Public: boolPtr(true), + ParentUID: stringPtr("787620d0-d7de-449a-b0bf-9d28b13da818"), + Auditors: []string{"user1"}, + Writers: []string{"user2"}, + }, + setupMocks: func(service *ProjectsService) { + mockKV := service.projectsKV.(*nats.MockKeyValue) + mockKV.On("Get", mock.Anything, "787620d0-d7de-449a-b0bf-9d28b13da818").Return(&nats.MockKeyValueEntry{}, jetstream.ErrKeyNotFound) + }, + expectedError: true, + }, + { + name: "slug already exists", + payload: &projsvc.CreateProjectPayload{ + Slug: "existing-project", + Name: "Test Project", + Description: "Test description", + Public: boolPtr(true), + ParentUID: stringPtr(""), + Auditors: []string{"user1"}, + Writers: []string{"user2"}, + }, + setupMocks: func(service *ProjectsService) { + mockKV := service.projectsKV.(*nats.MockKeyValue) + mockKV.On("Put", mock.Anything, "slug/existing-project", mock.Anything).Return(uint64(1), jetstream.ErrKeyExists) + }, + expectedError: true, + }, + { + name: "error creating slug mapping", + payload: &projsvc.CreateProjectPayload{ + Slug: "test-project", + Name: "Test Project", + Description: "Test description", + Public: boolPtr(true), + ParentUID: stringPtr(""), + Auditors: []string{"user1"}, + Writers: []string{"user2"}, + }, + setupMocks: func(service *ProjectsService) { + mockKV := service.projectsKV.(*nats.MockKeyValue) + mockKV.On("Put", mock.Anything, "slug/test-project", mock.Anything).Return(uint64(1), assert.AnError) + }, + expectedError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + service := setupService() + tt.setupMocks(service) + + result, err := service.CreateProject(context.Background(), tt.payload) + + if tt.expectedError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, tt.payload.Slug, *result.Slug) + assert.Equal(t, tt.payload.Name, *result.Name) + assert.Equal(t, tt.payload.Description, *result.Description) + assert.Equal(t, tt.payload.Public, result.Public) + assert.Equal(t, tt.payload.ParentUID, result.ParentUID) + assert.Equal(t, tt.payload.Auditors, result.Auditors) + assert.Equal(t, tt.payload.Writers, result.Writers) + assert.NotEmpty(t, *result.ID) + } + }) + } +} + +func TestGetOneProject(t *testing.T) { + tests := []struct { + name string + payload *projsvc.GetOneProjectPayload + setupMocks func(*nats.MockKeyValue) + expectedError bool + expectedID string + }{ + { + name: "success", + payload: &projsvc.GetOneProjectPayload{ + ID: stringPtr("project-1"), + }, + setupMocks: func(mockKV *nats.MockKeyValue) { + projectData := `{"uid":"project-1","slug":"test-1","name":"Test Project","description":"Test description","public":true,"parent_uid":"","auditors":["user1"],"writers":["user2"],"created_at":"2023-01-01T00:00:00Z","updated_at":"2023-01-01T00:00:00Z"}` + mockKV.On("Get", mock.Anything, "project-1").Return(nats.NewMockKeyValueEntry([]byte(projectData), 123), nil) + }, + expectedError: false, + expectedID: "project-1", + }, + { + name: "project not found", + payload: &projsvc.GetOneProjectPayload{ + ID: stringPtr("nonexistent"), + }, + setupMocks: func(mockKV *nats.MockKeyValue) { + mockKV.On("Get", mock.Anything, "nonexistent").Return(&nats.MockKeyValueEntry{}, jetstream.ErrKeyNotFound) + }, + expectedError: true, + }, + { + name: "error getting project", + payload: &projsvc.GetOneProjectPayload{ + ID: stringPtr("project-1"), + }, + setupMocks: func(mockKV *nats.MockKeyValue) { + mockKV.On("Get", mock.Anything, "project-1").Return(&nats.MockKeyValueEntry{}, assert.AnError) + }, + expectedError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + service := setupService() + tt.setupMocks(service.projectsKV.(*nats.MockKeyValue)) + + result, err := service.GetOneProject(context.Background(), tt.payload) + + if tt.expectedError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + if assert.NotNil(t, result) { + if assert.NotNil(t, result.Project) { + assert.Equal(t, tt.expectedID, *result.Project.ID) + } + } + if assert.NotNil(t, result.Etag) { + assert.NotEmpty(t, *result.Etag) + } + } + }) + } +} + +func TestUpdateProject(t *testing.T) { + tests := []struct { + name string + payload *projsvc.UpdateProjectPayload + setupMocks func(*ProjectsService) + expectedError bool + }{ + { + name: "success", + payload: &projsvc.UpdateProjectPayload{ + Etag: stringPtr("1"), + ID: stringPtr("project-1"), + Slug: "updated-slug", + Name: "Updated Project", + Description: "Updated description", + Public: boolPtr(true), + ParentUID: stringPtr(""), + Auditors: []string{"user1", "user2"}, + Writers: []string{"user3", "user4"}, + }, + setupMocks: func(service *ProjectsService) { + mockNats := service.natsConn.(*nats.MockNATSConn) + mockKV := service.projectsKV.(*nats.MockKeyValue) + // Mock getting existing project + projectData := `{"uid":"project-1","slug":"old-slug","name":"Old Project","description":"Old description","public":false,"parent_uid":"parent-uid","auditors":["user1"],"writers":["user2"],"created_at":"2023-01-01T00:00:00Z","updated_at":"2023-01-01T00:00:00Z"}` + mockKV.On("Get", mock.Anything, "project-1").Return(nats.NewMockKeyValueEntry([]byte(projectData), 1), nil) + // Mock updating project + mockKV.On("Update", mock.Anything, "project-1", mock.Anything, uint64(1)).Return(uint64(1), nil) + mockNats.On("Publish", fmt.Sprintf("%s%s", service.lfxEnvironment, constants.IndexProjectSubject), mock.Anything).Return(nil) + mockNats.On("Publish", fmt.Sprintf("%s%s", service.lfxEnvironment, constants.UpdateAccessProjectSubject), mock.Anything).Return(nil) + }, + expectedError: false, + }, + { + name: "etag header is invalid", + payload: &projsvc.UpdateProjectPayload{ + ID: stringPtr("project-1"), + Etag: stringPtr("invalid"), + }, + setupMocks: func(service *ProjectsService) { + mockKV := service.projectsKV.(*nats.MockKeyValue) + mockKV.On("Get", mock.Anything, "project-1").Return(nats.NewMockKeyValueEntry([]byte("test"), 1), nil) + mockKV.On("Update", mock.Anything, "project-1", mock.Anything, uint64(1)).Return(uint64(1), assert.AnError) + }, + expectedError: true, + }, + { + name: "invalid parent UID", + payload: &projsvc.UpdateProjectPayload{ + ID: stringPtr("project-1"), + Slug: "test", + Name: "Test", + Public: boolPtr(true), + ParentUID: stringPtr("invalid-parent-uid"), + Auditors: []string{"user1"}, + Writers: []string{"user2"}, + }, + setupMocks: func(_ *ProjectsService) {}, + expectedError: true, + }, + { + name: "parent project not found", + payload: &projsvc.UpdateProjectPayload{ + ID: stringPtr("project-1"), + Slug: "test", + Name: "Test", + Public: boolPtr(true), + ParentUID: stringPtr("787620d0-d7de-449a-b0bf-9d28b13da818"), + Auditors: []string{"user1"}, + Writers: []string{"user2"}, + }, + setupMocks: func(service *ProjectsService) { + mockKV := service.projectsKV.(*nats.MockKeyValue) + mockKV.On("Get", mock.Anything, "787620d0-d7de-449a-b0bf-9d28b13da818").Return(&nats.MockKeyValueEntry{}, jetstream.ErrKeyNotFound) + }, + expectedError: true, + }, + { + name: "project not found", + payload: &projsvc.UpdateProjectPayload{ + ID: stringPtr("nonexistent"), + Slug: "test", + Name: "Test", + Public: boolPtr(true), + ParentUID: stringPtr(""), + Auditors: []string{"user1"}, + Writers: []string{"user2"}, + }, + setupMocks: func(service *ProjectsService) { + mockKV := service.projectsKV.(*nats.MockKeyValue) + mockKV.On("Get", mock.Anything, "nonexistent").Return(&nats.MockKeyValueEntry{}, jetstream.ErrKeyNotFound) + }, + expectedError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + service := setupService() + tt.setupMocks(service) + + result, err := service.UpdateProject(context.Background(), tt.payload) + + if tt.expectedError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + if assert.NotNil(t, result) { + assert.Equal(t, tt.payload.Slug, *result.Slug) + assert.Equal(t, tt.payload.Name, *result.Name) + assert.Equal(t, tt.payload.Description, *result.Description) + assert.Equal(t, tt.payload.Public, result.Public) + assert.Equal(t, tt.payload.ParentUID, result.ParentUID) + assert.Equal(t, tt.payload.Auditors, result.Auditors) + assert.Equal(t, tt.payload.Writers, result.Writers) + } + } + }) + } +} + +func TestDeleteProject(t *testing.T) { + tests := []struct { + name string + payload *projsvc.DeleteProjectPayload + setupMocks func(*ProjectsService) + expectedError bool + }{ + { + name: "success", + payload: &projsvc.DeleteProjectPayload{ + ID: stringPtr("project-1"), + Etag: stringPtr("1"), + }, + setupMocks: func(service *ProjectsService) { + mockNats := service.natsConn.(*nats.MockNATSConn) + mockKV := service.projectsKV.(*nats.MockKeyValue) + // Mock getting existing project + projectData := `{"uid":"project-1","slug":"test","name":"Test Project","description":"Test","public":false,"parent_uid":"parent-uid","auditors":["user1"],"writers":["user2"],"created_at":"2023-01-01T00:00:00Z","updated_at":"2023-01-01T00:00:00Z"}` + mockKV.On("Get", mock.Anything, "project-1").Return(nats.NewMockKeyValueEntry([]byte(projectData), 1), nil) + // Mock deleting project + mockKV.On("Delete", mock.Anything, "project-1", mock.Anything).Return(nil) + mockNats.On("Publish", fmt.Sprintf("%s%s", service.lfxEnvironment, constants.IndexProjectSubject), mock.Anything).Return(nil) + mockNats.On("Publish", fmt.Sprintf("%s%s", service.lfxEnvironment, constants.DeleteAllAccessSubject), mock.Anything).Return(nil) + }, + expectedError: false, + }, + { + name: "etag header is invalid", + payload: &projsvc.DeleteProjectPayload{ + ID: stringPtr("project-1"), + Etag: stringPtr("invalid"), + }, + setupMocks: func(service *ProjectsService) { + mockKV := service.projectsKV.(*nats.MockKeyValue) + mockKV.On("Get", mock.Anything, "project-1").Return(nats.NewMockKeyValueEntry([]byte("test"), 1), nil) + mockKV.On("Delete", mock.Anything, "project-1", mock.Anything).Return(assert.AnError) + }, + expectedError: true, + }, + { + name: "project not found", + payload: &projsvc.DeleteProjectPayload{ + ID: stringPtr("nonexistent"), + }, + setupMocks: func(service *ProjectsService) { + mockKV := service.projectsKV.(*nats.MockKeyValue) + mockKV.On("Get", mock.Anything, "nonexistent").Return(&nats.MockKeyValueEntry{}, jetstream.ErrKeyNotFound) + }, + expectedError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + service := setupService() + tt.setupMocks(service) + + err := service.DeleteProject(context.Background(), tt.payload) + + if tt.expectedError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/cmd/project-api/service_endpoint_test.go b/cmd/project-api/service_endpoint_test.go new file mode 100644 index 0000000..abc574d --- /dev/null +++ b/cmd/project-api/service_endpoint_test.go @@ -0,0 +1,133 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +package main + +import ( + "context" + "testing" + + "github.com/linuxfoundation/lfx-v2-project-service/internal/infrastructure/nats" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "goa.design/goa/v3/security" +) + +func TestReadyz(t *testing.T) { + tests := []struct { + name string + setupMocks func(*ProjectsService) + expectedError bool + expectedBody string + }{ + { + name: "service ready", + setupMocks: func(service *ProjectsService) { + service.natsConn.(*nats.MockNATSConn).On("IsConnected").Return(true) + }, + expectedError: false, + expectedBody: "OK\n", + }, + { + name: "NATS not connected", + setupMocks: func(service *ProjectsService) { + service.natsConn.(*nats.MockNATSConn).On("IsConnected").Return(false) + }, + expectedError: true, + }, + { + name: "NATS KV not initialized", + setupMocks: func(service *ProjectsService) { + service.projectsKV = nil + }, + expectedError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + service := setupService() + tt.setupMocks(service) + + result, err := service.Readyz(context.Background()) + + if tt.expectedError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedBody, string(result)) + } + }) + } +} + +func TestLivez(t *testing.T) { + service := &ProjectsService{} + + result, err := service.Livez(context.Background()) + + assert.NoError(t, err) + assert.Equal(t, "OK\n", string(result)) +} + +func TestJWTAuth(t *testing.T) { + tests := []struct { + name string + bearerToken string + schema *security.JWTScheme + expectedError bool + setupMocks func(*MockJwtAuth) + }{ + { + name: "valid token", + // This token is just an example token value generated from jwt.io. + bearerToken: "eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.iOeNU4dAFFeBwNj6qdhdvm-IvDQrTa6R22lQVJVuWJxorJfeQww5Nwsra0PjaOYhAMj9jNMO5YLmud8U7iQ5gJK2zYyepeSuXhfSi8yjFZfRiSkelqSkU19I-Ja8aQBDbqXf2SAWA8mHF8VS3F08rgEaLCyv98fLLH4vSvsJGf6ueZSLKDVXz24rZRXGWtYYk_OYYTVgR1cg0BLCsuCvqZvHleImJKiWmtS0-CymMO4MMjCy_FIl6I56NqLE9C87tUVpo1mT-kbg5cHDD8I7MjCW5Iii5dethB4Vid3mZ6emKjVYgXrtkOQ-JyGMh6fnQxEFN1ft33GX2eRHluK9eg", + schema: &security.JWTScheme{}, + expectedError: false, + setupMocks: func(mockJwtAuth *MockJwtAuth) { + mockJwtAuth.On("parsePrincipal", mock.Anything, mock.Anything, mock.Anything).Return("user1", nil) + }, + }, + { + name: "invalid token", + bearerToken: "invalid.token", + schema: &security.JWTScheme{}, + expectedError: true, + setupMocks: func(mockJwtAuth *MockJwtAuth) { + mockJwtAuth.On("parsePrincipal", mock.Anything, mock.Anything, mock.Anything).Return("", assert.AnError) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + service := setupService() + tt.setupMocks(service.auth.(*MockJwtAuth)) + + ctx, err := service.JWTAuth(context.Background(), tt.bearerToken, tt.schema) + + if tt.expectedError { + assert.Error(t, err) + } else if assert.NoError(t, err) { + // For valid tokens, we expect the context to be modified + assert.NotEqual(t, context.Background(), ctx) + } + }) + } +} + +// Helper function to create string pointers +func stringPtr(s string) *string { + return &s +} + +// Helper function to create boolean pointers +func boolPtr(b bool) *bool { + return &b +} + +// Test cleanup +func TestMain(m *testing.M) { + // Run tests + m.Run() +} diff --git a/cmd/project-api/service_handler.go b/cmd/project-api/service_handler.go new file mode 100644 index 0000000..dc56ad3 --- /dev/null +++ b/cmd/project-api/service_handler.go @@ -0,0 +1,142 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +package main + +import ( + "context" + "fmt" + "log/slog" + + "github.com/google/uuid" + "github.com/linuxfoundation/lfx-v2-project-service/internal/log" + "github.com/linuxfoundation/lfx-v2-project-service/pkg/constants" + "github.com/nats-io/nats.go" +) + +// INatsMsg is an interface for [nats.Msg] that allows for mocking. +type INatsMsg interface { + Respond(data []byte) error + Data() []byte + Subject() string +} + +// NatsMsg is a wrapper around [nats.Msg] that implements [INatsMsg]. +type NatsMsg struct { + *nats.Msg +} + +// Respond implements [INatsMsg.Respond]. +func (m *NatsMsg) Respond(data []byte) error { + return m.Msg.Respond(data) +} + +// Data implements [INatsMsg.Data]. +func (m *NatsMsg) Data() []byte { + return m.Msg.Data +} + +// Subject implements [INatsMsg.Subject]. +func (m *NatsMsg) Subject() string { + return m.Msg.Subject +} + +// HandleNatsMessage is the entrypoint NATS handler for all subjects handled by the service. +func (s *ProjectsService) HandleNatsMessage(msg INatsMsg) { + subject := msg.Subject() + ctx := log.AppendCtx(context.Background(), slog.String("subject", subject)) + slog.DebugContext(ctx, "handling NATS message") + + var response []byte + var err error + switch subject { + case fmt.Sprintf("%s%s", s.lfxEnvironment, constants.ProjectGetNameSubject): + response, err = s.HandleProjectGetName(msg) + if err != nil { + slog.ErrorContext(ctx, "error handling project get name", errKey, err) + err = msg.Respond(nil) + if err != nil { + slog.ErrorContext(ctx, "error responding to NATS message", errKey, err) + } + return + } + err = msg.Respond(response) + if err != nil { + slog.ErrorContext(ctx, "error responding to NATS message", errKey, err) + return + } + case fmt.Sprintf("%s%s", s.lfxEnvironment, constants.ProjectSlugToUIDSubject): + response, err = s.HandleProjectSlugToUID(msg) + if err != nil { + slog.ErrorContext(ctx, "error handling project slug to UID", errKey, err) + err = msg.Respond(nil) + if err != nil { + slog.ErrorContext(ctx, "error responding to NATS message", errKey, err) + } + return + } + err = msg.Respond(response) + if err != nil { + slog.ErrorContext(ctx, "error responding to NATS message", errKey, err) + return + } + default: + slog.WarnContext(ctx, "unknown subject") + err = msg.Respond(nil) + if err != nil { + slog.ErrorContext(ctx, "error responding to NATS message", errKey, err) + return + } + } + + slog.DebugContext(ctx, "responded to NATS message", "response", response) +} + +// HandleProjectGetName is the NATS handler for the project-get-name subject. +func (s *ProjectsService) HandleProjectGetName(msg INatsMsg) ([]byte, error) { + projectID := string(msg.Data()) + + ctx := log.AppendCtx(context.Background(), slog.String("project_id", projectID)) + ctx = log.AppendCtx(ctx, slog.String("subject", constants.ProjectGetNameSubject)) + + // Validate that the project ID is a valid UUID. + _, err := uuid.Parse(projectID) + if err != nil { + slog.ErrorContext(ctx, "error parsing project ID", errKey, err) + return nil, err + } + + if s.projectsKV == nil { + slog.ErrorContext(ctx, "NATS KV store not initialized") + return nil, fmt.Errorf("NATS KV store not initialized") + } + + project, err := s.projectsKV.Get(ctx, projectID) + if err != nil { + slog.ErrorContext(ctx, "error getting project from NATS KV", errKey, err) + return nil, err + } + + return project.Value(), nil +} + +// HandleProjectSlugToUID is the NATS handler for the project-slug-to-uid subject. +func (s *ProjectsService) HandleProjectSlugToUID(msg INatsMsg) ([]byte, error) { + projectSlug := string(msg.Data()) + + ctx := log.AppendCtx(context.Background(), slog.String("project_slug", projectSlug)) + ctx = log.AppendCtx(ctx, slog.String("subject", constants.ProjectSlugToUIDSubject)) + + if s.projectsKV == nil { + slog.ErrorContext(ctx, "NATS KV store not initialized") + return nil, fmt.Errorf("NATS KV store not initialized") + } + + project, err := s.projectsKV.Get(ctx, fmt.Sprintf("slug/%s", projectSlug)) + if err != nil { + slog.ErrorContext(ctx, "error getting project from NATS KV", errKey, err) + return nil, err + } + + return project.Value(), nil +} diff --git a/cmd/project-api/service_handler_test.go b/cmd/project-api/service_handler_test.go new file mode 100644 index 0000000..f14dcee --- /dev/null +++ b/cmd/project-api/service_handler_test.go @@ -0,0 +1,225 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +package main + +import ( + "fmt" + "testing" + + "github.com/linuxfoundation/lfx-v2-project-service/internal/infrastructure/nats" + "github.com/linuxfoundation/lfx-v2-project-service/pkg/constants" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +type MockNatsMsg struct { + mock.Mock + data []byte + subject string +} + +func (m *MockNatsMsg) Respond(data []byte) error { + args := m.Called(data) + return args.Error(0) +} + +func (m *MockNatsMsg) Data() []byte { + return m.data +} + +func (m *MockNatsMsg) Subject() string { + return m.subject +} + +// CreateMockNatsMsg creates a mock NATS message that can be used in tests +func CreateMockNatsMsg(data []byte) *MockNatsMsg { + msg := MockNatsMsg{ + data: data, + } + return &msg +} + +// CreateMockNatsMsgWithSubject creates a mock NATS message with a specific subject +func CreateMockNatsMsgWithSubject(data []byte, subject string) *MockNatsMsg { + msg := MockNatsMsg{ + data: data, + subject: subject, + } + return &msg +} + +// TestHandleProjectGetName tests the [HandleProjectGetName] function. +func TestHandleProjectGetName(t *testing.T) { + tests := []struct { + name string + projectID string + setupMocks func(*ProjectsService, *MockNatsMsg) + expectedError bool + }{ + { + name: "success", + projectID: "550e8400-e29b-41d4-a716-446655440000", // Valid UUID + setupMocks: func(service *ProjectsService, _ *MockNatsMsg) { + projectData := `{"uid":"550e8400-e29b-41d4-a716-446655440000","slug":"test-project","name":"Test Project","description":"Test description","public":true,"parent_uid":"","auditors":["user1"],"writers":["user2"],"created_at":"2023-01-01T00:00:00Z","updated_at":"2023-01-01T00:00:00Z"}` + service.projectsKV.(*nats.MockKeyValue).On("Get", mock.Anything, "550e8400-e29b-41d4-a716-446655440000").Return(nats.NewMockKeyValueEntry([]byte(projectData), 123), nil) + }, + expectedError: false, + }, + { + name: "invalid UUID", + projectID: "invalid-uuid", + setupMocks: func(_ *ProjectsService, _ *MockNatsMsg) { + // No mocks needed for invalid UUID case + }, + expectedError: true, + }, + { + name: "NATS KV not initialized", + projectID: "550e8400-e29b-41d4-a716-446655440000", + setupMocks: func(service *ProjectsService, _ *MockNatsMsg) { + service.projectsKV = nil + }, + expectedError: true, + }, + { + name: "error getting project", + projectID: "550e8400-e29b-41d4-a716-446655440000", + setupMocks: func(service *ProjectsService, _ *MockNatsMsg) { + service.projectsKV.(*nats.MockKeyValue).On("Get", mock.Anything, "550e8400-e29b-41d4-a716-446655440000").Return(&nats.MockKeyValueEntry{}, assert.AnError) + }, + expectedError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + service := setupService() + msg := CreateMockNatsMsg([]byte(tt.projectID)) + tt.setupMocks(service, msg) + + // Test that the function doesn't panic and handles the message + assert.NotPanics(t, func() { + _, err := service.HandleProjectGetName(msg) + if tt.expectedError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + + // For success case, verify that the mock was called as expected + if tt.name == "success" { + service.projectsKV.(*nats.MockKeyValue).AssertExpectations(t) + } + }) + } +} + +// TestHandleProjectSlugToUID tests the [HandleProjectSlugToUID] function. +func TestHandleProjectSlugToUID(t *testing.T) { + tests := []struct { + name string + projectSlug string + setupMocks func(*ProjectsService, *MockNatsMsg) + expectedError bool + }{ + { + name: "success", + projectSlug: "test-project", + setupMocks: func(service *ProjectsService, _ *MockNatsMsg) { + projectUID := "550e8400-e29b-41d4-a716-446655440000" + service.projectsKV.(*nats.MockKeyValue).On("Get", mock.Anything, "slug/test-project").Return(nats.NewMockKeyValueEntry([]byte(projectUID), 123), nil) + }, + expectedError: false, + }, + { + name: "NATS KV not initialized", + projectSlug: "test-project", + setupMocks: func(service *ProjectsService, _ *MockNatsMsg) { + service.projectsKV = nil + }, + expectedError: true, + }, + { + name: "error getting project", + projectSlug: "test-project", + setupMocks: func(service *ProjectsService, _ *MockNatsMsg) { + service.projectsKV.(*nats.MockKeyValue).On("Get", mock.Anything, "slug/test-project").Return(&nats.MockKeyValueEntry{}, assert.AnError) + }, + expectedError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + service := setupService() + msg := CreateMockNatsMsg([]byte(tt.projectSlug)) + tt.setupMocks(service, msg) + + // Test that the function doesn't panic and handles the message + assert.NotPanics(t, func() { + _, err := service.HandleProjectSlugToUID(msg) + if tt.expectedError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + + // For success case, verify that the mock was called as expected + if tt.name == "success" { + service.projectsKV.(*nats.MockKeyValue).AssertExpectations(t) + } + }) + } +} + +// TestHandleNatsMessage tests the [HandleNatsMessage] function. +func TestHandleNatsMessage(t *testing.T) { + tests := []struct { + name string + subject string + data []byte + wantNil bool // if true, expect Respond(nil) + }{ + { + name: "project get name routes and responds", + subject: fmt.Sprintf("%s%s", constants.LFXEnvironmentDev, constants.IndexProjectSubject), + data: []byte("some-id"), + wantNil: false, + }, + { + name: "project slug to UID routes and responds", + subject: fmt.Sprintf("%s%s", constants.LFXEnvironmentDev, constants.UpdateAccessProjectSubject), + data: []byte("some-slug"), + wantNil: false, + }, + { + name: "unknown subject responds nil", + subject: "unknown.subject", + data: []byte("test-data"), + wantNil: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + service := setupService() + msg := CreateMockNatsMsgWithSubject(tt.data, tt.subject) + if tt.wantNil { + msg.On("Respond", []byte(nil)).Return(nil).Once() + } else { + msg.On("Respond", mock.Anything).Return(nil).Once() + // Set up a generic expectation for the Get method to avoid mock panics + service.projectsKV.(*nats.MockKeyValue).On("Get", mock.Anything, mock.Anything).Return(&nats.MockKeyValueEntry{}, nil) + } + + assert.NotPanics(t, func() { + service.HandleNatsMessage(msg) + }) + + msg.AssertExpectations(t) + }) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..a3615c1 --- /dev/null +++ b/go.mod @@ -0,0 +1,35 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT +module github.com/linuxfoundation/lfx-v2-project-service + +go 1.23.0 + +require ( + github.com/auth0/go-jwt-middleware/v2 v2.3.0 + github.com/google/uuid v1.6.0 + github.com/nats-io/nats.go v1.43.0 + github.com/stretchr/testify v1.10.0 + goa.design/goa/v3 v3.21.1 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dimfeld/httppath v0.0.0-20170720192232-ee938bf73598 // indirect + github.com/go-chi/chi/v5 v5.2.2 // indirect + github.com/gohugoio/hashstructure v0.5.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/manveru/faker v0.0.0-20171103152722-9fbc68a78c4d // indirect + github.com/nats-io/nkeys v0.4.11 // indirect + github.com/nats-io/nuid v1.0.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect + golang.org/x/crypto v0.40.0 // indirect + golang.org/x/mod v0.25.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/sys v0.34.0 // indirect + golang.org/x/text v0.27.0 // indirect + golang.org/x/tools v0.34.0 // indirect + gopkg.in/go-jose/go-jose.v2 v2.6.3 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..9f70754 --- /dev/null +++ b/go.sum @@ -0,0 +1,56 @@ +github.com/auth0/go-jwt-middleware/v2 v2.3.0 h1:4QREj6cS3d8dS05bEm443jhnqQF97FX9sMBeWqnNRzE= +github.com/auth0/go-jwt-middleware/v2 v2.3.0/go.mod h1:dL4ObBs1/dj4/W4cYxd8rqAdDGXYyd5rqbpMIxcbVrU= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dimfeld/httppath v0.0.0-20170720192232-ee938bf73598 h1:MGKhKyiYrvMDZsmLR/+RGffQSXwEkXgfLSA08qDn9AI= +github.com/dimfeld/httppath v0.0.0-20170720192232-ee938bf73598/go.mod h1:0FpDmbrt36utu8jEmeU05dPC9AB5tsLYVVi+ZHfyuwI= +github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618= +github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +github.com/gohugoio/hashstructure v0.5.0 h1:G2fjSBU36RdwEJBWJ+919ERvOVqAg9tfcYp47K9swqg= +github.com/gohugoio/hashstructure v0.5.0/go.mod h1:Ser0TniXuu/eauYmrwM4o64EBvySxNzITEOLlm4igec= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/manveru/faker v0.0.0-20171103152722-9fbc68a78c4d h1:Zj+PHjnhRYWBK6RqCDBcAhLXoi3TzC27Zad/Vn+gnVQ= +github.com/manveru/faker v0.0.0-20171103152722-9fbc68a78c4d/go.mod h1:WZy8Q5coAB1zhY9AOBJP0O6J4BuDfbupUDavKY+I3+s= +github.com/manveru/gobdd v0.0.0-20131210092515-f1a17fdd710b h1:3E44bLeN8uKYdfQqVQycPnaVviZdBLbizFhU49mtbe4= +github.com/manveru/gobdd v0.0.0-20131210092515-f1a17fdd710b/go.mod h1:Bj8LjjP0ReT1eKt5QlKjwgi5AFm5mI6O1A2G4ChI0Ag= +github.com/nats-io/nats.go v1.43.0 h1:uRFZ2FEoRvP64+UUhaTokyS18XBCR/xM2vQZKO4i8ug= +github.com/nats-io/nats.go v1.43.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g= +github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0= +github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +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/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +goa.design/goa/v3 v3.21.1 h1:tLwhbcNoEBJm1CcJc3ks6oZ8BHYl6vFuxEBnl2kC428= +goa.design/goa/v3 v3.21.1/go.mod h1:E+97AYffVIvDi6LkuNdfdvMZb8UFb/+ie3V0/WBBdgc= +golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= +golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= +golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= +golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= +golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/go-jose/go-jose.v2 v2.6.3 h1:nt80fvSDlhKWQgSWyHyy5CfmlQr+asih51R8PTWNKKs= +gopkg.in/go-jose/go-jose.v2 v2.6.3/go.mod h1:zzZDPkNNw/c9IE7Z9jr11mBZQhKQTMzoEEIoEdZlFBI= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/infrastructure/nats/message.go b/internal/infrastructure/nats/message.go new file mode 100644 index 0000000..e2dae07 --- /dev/null +++ b/internal/infrastructure/nats/message.go @@ -0,0 +1,78 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +package nats + +import ( + "context" + "encoding/json" + "fmt" + "log/slog" + + "github.com/linuxfoundation/lfx-v2-project-service/pkg/constants" +) + +// MessageBuilder is the builder for the transaction and sends it to the NATS server. +type MessageBuilder struct { + NatsConn INatsConn + LfxEnvironment constants.LFXEnvironment +} + +// SendIndexingTransaction sends the transaction to the NATS server for the data indexing. +func (m *MessageBuilder) SendIndexProjectTransaction(ctx context.Context, action TransactionAction, data []byte) error { + subject := fmt.Sprintf("%s%s", m.LfxEnvironment, constants.IndexProjectSubject) + + headers := make(map[string]string) + if authorization, ok := ctx.Value(constants.AuthorizationContextID).(string); ok { + headers["authorization"] = authorization + } + if principal, ok := ctx.Value(constants.PrincipalContextID).(string); ok { + headers["x-on-behalf-of"] = principal + } + + transaction := ProjectTransaction{ + Action: action, + Headers: headers, + Data: data, + } + + transactionBytes, err := json.Marshal(transaction) + if err != nil { + slog.ErrorContext(ctx, "error marshalling transaction into JSON", constants.ErrKey, err) + return err + } + + err = m.NatsConn.Publish(subject, transactionBytes) + if err != nil { + slog.ErrorContext(ctx, "error sending transaction to NATS", constants.ErrKey, err, "subject", subject) + return err + } + slog.DebugContext(ctx, "sent transaction to NATS for data indexing", "subject", subject) + return nil +} + +// SendUpdateAccessTransaction sends the transaction to the NATS server for the access control updates. +func (m *MessageBuilder) SendUpdateAccessProjectTransaction(ctx context.Context, data []byte) error { + // Send the transaction to the NATS server for the access control updates. + subject := fmt.Sprintf("%s%s", m.LfxEnvironment, constants.UpdateAccessProjectSubject) + err := m.NatsConn.Publish(subject, data) + if err != nil { + slog.ErrorContext(ctx, "error sending transaction to NATS", constants.ErrKey, err, "subject", subject) + return err + } + slog.DebugContext(ctx, "sent transaction to NATS for project access control updates", "subject", subject) + return nil +} + +// SendDeleteAllAccessProjectTransaction sends the transaction to the NATS server for the access control deletion. +func (m *MessageBuilder) SendDeleteAllAccessProjectTransaction(ctx context.Context, data []byte) error { + // Send the transaction to the NATS server for the access control deletion. + subject := fmt.Sprintf("%s%s", m.LfxEnvironment, constants.DeleteAllAccessSubject) + err := m.NatsConn.Publish(subject, data) + if err != nil { + slog.ErrorContext(ctx, "error sending transaction to NATS", constants.ErrKey, err, "subject", subject) + return err + } + slog.DebugContext(ctx, "sent transaction to NATS for project access control deletion", "subject", subject) + return nil +} diff --git a/internal/infrastructure/nats/mock.go b/internal/infrastructure/nats/mock.go new file mode 100644 index 0000000..95c37a3 --- /dev/null +++ b/internal/infrastructure/nats/mock.go @@ -0,0 +1,163 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +package nats + +import ( + "context" + "time" + + "github.com/nats-io/nats.go/jetstream" + "github.com/stretchr/testify/mock" +) + +// INatsConn is a NATS connection interface needed for the [ProjectsService]. +type INatsConn interface { + IsConnected() bool + Publish(subj string, data []byte) error +} + +// MockNATSConn is a mock implementation of the [INatsConn] interface. +type MockNATSConn struct { + mock.Mock +} + +// IsConnected is a mock method for the [INatsConn] interface. +func (m *MockNATSConn) IsConnected() bool { + args := m.Called() + return args.Bool(0) +} + +// Publish is a mock method for the [INatsConn] interface. +func (m *MockNATSConn) Publish(subj string, data []byte) error { + args := m.Called(subj, data) + return args.Error(0) +} + +// INatsKeyValue is a NATS KV interface needed for the [ProjectsService]. +type INatsKeyValue interface { + ListKeys(context.Context, ...jetstream.WatchOpt) (jetstream.KeyLister, error) + Get(ctx context.Context, key string) (jetstream.KeyValueEntry, error) + Put(context.Context, string, []byte) (uint64, error) + Update(context.Context, string, []byte, uint64) (uint64, error) + Delete(context.Context, string, ...jetstream.KVDeleteOpt) error +} + +// MockKeyValue is a mock implementation of the [INatsKeyValue] interface. +type MockKeyValue struct { + mock.Mock +} + +// Put is a mock method for the [INatsKeyValue] interface. +func (m *MockKeyValue) Put(ctx context.Context, key string, value []byte) (uint64, error) { + args := m.Called(ctx, key, value) + return args.Get(0).(uint64), args.Error(1) +} + +// Get is a mock method for the [INatsKeyValue] interface. +func (m *MockKeyValue) Get(ctx context.Context, key string) (jetstream.KeyValueEntry, error) { + args := m.Called(ctx, key) + return args.Get(0).(jetstream.KeyValueEntry), args.Error(1) +} + +// Update is a mock method for the [INatsKeyValue] interface. +func (m *MockKeyValue) Update(ctx context.Context, key string, value []byte, revision uint64) (uint64, error) { + args := m.Called(ctx, key, value, revision) + return args.Get(0).(uint64), args.Error(1) +} + +// Delete is a mock method for the [INatsKeyValue] interface. +func (m *MockKeyValue) Delete(ctx context.Context, key string, opts ...jetstream.KVDeleteOpt) error { + args := m.Called(ctx, key, opts) + return args.Error(0) +} + +// ListKeys is a mock method for the [INatsKeyValue] interface. +func (m *MockKeyValue) ListKeys(ctx context.Context, _ ...jetstream.WatchOpt) (jetstream.KeyLister, error) { + args := m.Called(ctx) + return args.Get(0).(jetstream.KeyLister), args.Error(1) +} + +// MockKeyLister is a mock implementation of the [jetstream.KeyLister] interface. +type MockKeyLister struct { + mock.Mock + keys []string +} + +func NewMockKeyLister(keys []string) *MockKeyLister { + return &MockKeyLister{ + keys: keys, + } +} + +// Keys is a mock method for the [jetstream.KeyLister] interface. +func (m *MockKeyLister) Keys() <-chan string { + ch := make(chan string) + go func() { + defer close(ch) + for _, key := range m.keys { + ch <- key + } + }() + return ch +} + +// Stop is a mock method for the [jetstream.KeyLister] interface. +func (m *MockKeyLister) Stop() error { + args := m.Called() + return args.Error(0) +} + +// MockKeyValueEntry is a mock implementation of the [jetstream.KeyValueEntry] interface. +type MockKeyValueEntry struct { + mock.Mock + value []byte + revision uint64 +} + +func NewMockKeyValueEntry(value []byte, revision uint64) *MockKeyValueEntry { + return &MockKeyValueEntry{ + value: value, + revision: revision, + } +} + +// Key is a mock method for the [jetstream.KeyValueEntry] interface. +func (m *MockKeyValueEntry) Key() string { + args := m.Called() + return args.String(0) +} + +// Value is a mock method for the [jetstream.KeyValueEntry] interface. +func (m *MockKeyValueEntry) Value() []byte { + return m.value +} + +// Revision is a mock method for the [jetstream.KeyValueEntry] interface. +func (m *MockKeyValueEntry) Revision() uint64 { + return m.revision +} + +// Created is a mock method for the [jetstream.KeyValueEntry] interface. +func (m *MockKeyValueEntry) Created() time.Time { + args := m.Called() + return args.Get(0).(time.Time) +} + +// Delta is a mock method for the [jetstream.KeyValueEntry] interface. +func (m *MockKeyValueEntry) Delta() uint64 { + args := m.Called() + return args.Get(0).(uint64) +} + +// Operation is a mock method for the [jetstream.KeyValueEntry] interface. +func (m *MockKeyValueEntry) Operation() jetstream.KeyValueOp { + args := m.Called() + return args.Get(0).(jetstream.KeyValueOp) +} + +// Bucket is a mock method for the [jetstream.KeyValueEntry] interface. +func (m *MockKeyValueEntry) Bucket() string { + args := m.Called() + return args.String(0) +} diff --git a/internal/infrastructure/nats/models_kv.go b/internal/infrastructure/nats/models_kv.go new file mode 100644 index 0000000..93e581a --- /dev/null +++ b/internal/infrastructure/nats/models_kv.go @@ -0,0 +1,21 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +// Package nats contains the models for the NATS messaging. +package nats + +import "time" + +// ProjectDB is the key-value store representation of a project. +type ProjectDB struct { + UID string `json:"uid"` + Slug string `json:"slug"` + Name string `json:"name"` + Description string `json:"description"` + Public bool `json:"public"` + ParentUID string `json:"parent_uid"` + Auditors []string `json:"auditors"` + Writers []string `json:"writers"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} diff --git a/internal/infrastructure/nats/models_message.go b/internal/infrastructure/nats/models_message.go new file mode 100644 index 0000000..6f7d4cf --- /dev/null +++ b/internal/infrastructure/nats/models_message.go @@ -0,0 +1,25 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +// Package nats contains the models for the NATS messaging. +package nats + +// TransactionAction is a type for the action of a project transaction. +type TransactionAction string + +// TransactionAction constants for the action of a project transaction. +const ( + // ActionCreated is the action for a resource creation transaction. + ActionCreated TransactionAction = "created" + // ActionUpdated is the action for a resource update transaction. + ActionUpdated TransactionAction = "updated" + // ActionDeleted is the action for a resource deletion transaction. + ActionDeleted TransactionAction = "deleted" +) + +// ProjectTransaction is a NATS message schema for sending messages related to projects CRUD operations. +type ProjectTransaction struct { + Action TransactionAction `json:"action"` + Headers map[string]string `json:"headers"` + Data any `json:"data"` +} diff --git a/internal/log/log.go b/internal/log/log.go new file mode 100644 index 0000000..916046c --- /dev/null +++ b/internal/log/log.go @@ -0,0 +1,104 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +// Package log contains the logging functionality for the project service. +package log + +import ( + "context" + "log" + "log/slog" + "os" +) + +type ctxKey string + +const ( + slogFields ctxKey = "slog_fields" + logLevelDefault = slog.LevelDebug + + debug = "debug" + warn = "warn" + err = "error" + info = "info" +) + +type contextHandler struct { + slog.Handler +} + +// Handle adds contextual attributes to the Record before calling the underlying handler +func (h contextHandler) Handle(ctx context.Context, r slog.Record) error { + if attrs, ok := ctx.Value(slogFields).([]slog.Attr); ok { + for _, v := range attrs { + r.AddAttrs(v) + } + } + + return h.Handler.Handle(ctx, r) +} + +// AppendCtx adds an slog attribute to the provided context so that it will be +// included in any Record created with such context +func AppendCtx(parent context.Context, attr slog.Attr) context.Context { + if parent == nil { + parent = context.Background() + } + + if v, ok := parent.Value(slogFields).([]slog.Attr); ok { + v = append(v, attr) + return context.WithValue(parent, slogFields, v) + } + + v := []slog.Attr{} + v = append(v, attr) + return context.WithValue(parent, slogFields, v) +} + +// InitStructureLogConfig sets the structured log behavior +func InitStructureLogConfig() slog.Handler { + logOptions := &slog.HandlerOptions{} + var h slog.Handler + + configurations := map[string]func(){ + "options-logLevel": func() { + logLevel := os.Getenv("LOG_LEVEL") + switch logLevel { + case debug: + logOptions.Level = slog.LevelDebug + case warn: + logOptions.Level = slog.LevelWarn + case err: + logOptions.Level = slog.LevelError + case info: + logOptions.Level = slog.LevelInfo + default: + logOptions.Level = logLevelDefault + } + }, + "options-addSource": func() { + addSourceBool := false + addSource := os.Getenv("LOG_ADD_SOURCE") + if addSource == "true" || addSource == "t" || addSource == "1" { + addSourceBool = true + } + logOptions.AddSource = addSourceBool + }, + } + + for _, f := range configurations { + f() + } + + slog.Info("log config", + "logLevel", logOptions.Level, + "addSource", logOptions.AddSource, + ) + + h = slog.NewJSONHandler(os.Stdout, logOptions) + log.SetFlags(log.Llongfile) + logger := contextHandler{h} + slog.SetDefault(slog.New(logger)) + + return h +} diff --git a/internal/middleware/authorization.go b/internal/middleware/authorization.go new file mode 100644 index 0000000..66617df --- /dev/null +++ b/internal/middleware/authorization.go @@ -0,0 +1,30 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +package middleware + +import ( + "context" + "net/http" + + "github.com/linuxfoundation/lfx-v2-project-service/pkg/constants" +) + +// AuthorizationMiddleware creates a middleware that adds a request ID to the context +func AuthorizationMiddleware() func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Try to get authorization from the header + authorization := r.Header.Get(constants.AuthorizationHeader) + + // Add authorization to context + ctx := context.WithValue(r.Context(), constants.AuthorizationContextID, authorization) + + // Create a new request with the updated context + r = r.WithContext(ctx) + + // Call the next handler + next.ServeHTTP(w, r) + }) + } +} diff --git a/internal/middleware/authorization_test.go b/internal/middleware/authorization_test.go new file mode 100644 index 0000000..282bab0 --- /dev/null +++ b/internal/middleware/authorization_test.go @@ -0,0 +1,253 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +package middleware + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "sync" + "testing" + "time" + + "github.com/linuxfoundation/lfx-v2-project-service/pkg/constants" + "github.com/stretchr/testify/assert" +) + +func TestAuthorizationMiddleware(t *testing.T) { + tests := []struct { + name string + authorizationHeader string + expectInContext bool + expectedContextValue string + }{ + { + name: "adds bearer token to context", + authorizationHeader: "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9", + expectInContext: true, + expectedContextValue: "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9", + }, + { + name: "adds basic auth to context", + authorizationHeader: "Basic dXNlcjpwYXNzd29yZA==", + expectInContext: true, + expectedContextValue: "Basic dXNlcjpwYXNzd29yZA==", + }, + { + name: "handles empty authorization header", + authorizationHeader: "", + expectInContext: true, + expectedContextValue: "", + }, + { + name: "handles custom auth scheme", + authorizationHeader: "Custom some-custom-token", + expectInContext: true, + expectedContextValue: "Custom some-custom-token", + }, + { + name: "handles authorization with multiple spaces", + authorizationHeader: "Bearer token-with-spaces", + expectInContext: true, + expectedContextValue: "Bearer token-with-spaces", + }, + } + + assertion := assert.New(t) + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var capturedContext context.Context + var capturedAuthorization string + + // Test handler that captures the context and authorization value + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + capturedContext = r.Context() + capturedAuthorization = getAuthorizationFromContext(r.Context()) + w.WriteHeader(http.StatusOK) + }) + + // Wrap handler with Authorization middleware + middleware := AuthorizationMiddleware() + wrappedHandler := middleware(handler) + + // Create request + req := httptest.NewRequest("GET", "/test", nil) + if tc.authorizationHeader != "" { + req.Header.Set(constants.AuthorizationHeader, tc.authorizationHeader) + } + + rec := httptest.NewRecorder() + wrappedHandler.ServeHTTP(rec, req) + + // Verify authorization was added to context + if tc.expectInContext { + assertion.Equal(tc.expectedContextValue, capturedAuthorization) + } + + // Verify context contains authorization value + contextAuthorization := getAuthorizationFromContext(capturedContext) + assertion.Equal(tc.expectedContextValue, contextAuthorization) + }) + } +} + +func TestAuthorizationMiddlewareIntegration(t *testing.T) { + tests := []struct { + name string + setupRequest func(*http.Request) + checkContext func(*testing.T, context.Context) + expectedStatus int + }{ + { + name: "passes authorization through multiple handlers", + setupRequest: func(req *http.Request) { + req.Header.Set(constants.AuthorizationHeader, "Bearer test-token") + }, + checkContext: func(t *testing.T, ctx context.Context) { + auth := getAuthorizationFromContext(ctx) + assert.Equal(t, "Bearer test-token", auth) + }, + expectedStatus: http.StatusOK, + }, + { + name: "works with request ID middleware", + setupRequest: func(req *http.Request) { + req.Header.Set(constants.AuthorizationHeader, "Bearer integration-token") + req.Header.Set(constants.RequestIDHeader, "test-request-id") + }, + checkContext: func(t *testing.T, ctx context.Context) { + auth := getAuthorizationFromContext(ctx) + assert.Equal(t, "Bearer integration-token", auth) + + // Check if request ID is also in context (if both middlewares are used) + if requestID, ok := ctx.Value(constants.RequestIDContextID).(string); ok { + assert.Equal(t, "test-request-id", requestID) + } + }, + expectedStatus: http.StatusOK, + }, + { + name: "handles missing authorization header gracefully", + setupRequest: func(req *http.Request) { + // Don't set any authorization header + }, + checkContext: func(t *testing.T, ctx context.Context) { + auth := getAuthorizationFromContext(ctx) + assert.Empty(t, auth) + }, + expectedStatus: http.StatusOK, + }, + { + name: "preserves other headers", + setupRequest: func(req *http.Request) { + req.Header.Set(constants.AuthorizationHeader, "Bearer preserve-test") + req.Header.Set("X-Custom-Header", "custom-value") + req.Header.Set("Content-Type", "application/json") + }, + checkContext: func(t *testing.T, ctx context.Context) { + auth := getAuthorizationFromContext(ctx) + assert.Equal(t, "Bearer preserve-test", auth) + }, + expectedStatus: http.StatusOK, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var capturedContext context.Context + + // Test handler that captures the context + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + capturedContext = r.Context() + + // Verify other headers are preserved + if tc.name == "preserves other headers" { + assert.Equal(t, "custom-value", r.Header.Get("X-Custom-Header")) + assert.Equal(t, "application/json", r.Header.Get("Content-Type")) + } + + w.WriteHeader(tc.expectedStatus) + }) + + // Create middleware chain + authMiddleware := AuthorizationMiddleware() + requestIDMiddleware := RequestIDMiddleware() + + // Chain middlewares: RequestID -> Authorization -> Handler + wrappedHandler := authMiddleware(handler) + if tc.name == "works with request ID middleware" { + wrappedHandler = requestIDMiddleware(authMiddleware(handler)) + } + + // Create request + req := httptest.NewRequest("GET", "/test", nil) + tc.setupRequest(req) + + rec := httptest.NewRecorder() + wrappedHandler.ServeHTTP(rec, req) + + // Verify response status + assert.Equal(t, tc.expectedStatus, rec.Code) + + // Run context checks + tc.checkContext(t, capturedContext) + }) + } +} + +func TestAuthorizationMiddlewareConcurrency(t *testing.T) { + // Test that the middleware handles concurrent requests correctly + numRequests := 10 + tokens := make([]string, numRequests) + results := make([]string, numRequests) + + // Generate unique tokens + for i := 0; i < numRequests; i++ { + tokens[i] = fmt.Sprintf("Bearer token-%d", i) + } + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Simulate some processing time + time.Sleep(10 * time.Millisecond) + auth := getAuthorizationFromContext(r.Context()) + w.Write([]byte(auth)) + }) + + middleware := AuthorizationMiddleware() + wrappedHandler := middleware(handler) + + // Run concurrent requests + var wg sync.WaitGroup + for i := 0; i < numRequests; i++ { + wg.Add(1) + go func(index int) { + defer wg.Done() + + req := httptest.NewRequest("GET", "/test", nil) + req.Header.Set(constants.AuthorizationHeader, tokens[index]) + + rec := httptest.NewRecorder() + wrappedHandler.ServeHTTP(rec, req) + + results[index] = rec.Body.String() + }(i) + } + + wg.Wait() + + // Verify each request got its own authorization token + for i := 0; i < numRequests; i++ { + assert.Equal(t, tokens[i], results[i]) + } +} + +// Helper function to extract authorization from context +func getAuthorizationFromContext(ctx context.Context) string { + if authorization, ok := ctx.Value(constants.AuthorizationContextID).(string); ok { + return authorization + } + return "" +} diff --git a/internal/middleware/request_id.go b/internal/middleware/request_id.go new file mode 100644 index 0000000..326635c --- /dev/null +++ b/internal/middleware/request_id.go @@ -0,0 +1,53 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +// Package middleware contains the middleware to be used by services +package middleware + +import ( + "context" + "log/slog" + "net/http" + + "github.com/linuxfoundation/lfx-v2-project-service/internal/log" + "github.com/linuxfoundation/lfx-v2-project-service/pkg/constants" + + "github.com/google/uuid" +) + +// RequestIDMiddleware creates a middleware that adds a request ID to the context +func RequestIDMiddleware() func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Try to get request ID from header first + requestID := r.Header.Get(constants.RequestIDHeader) + + // If no request ID in header, generate a new one + if requestID == "" { + requestID = generateRequestID() + } + + // Add request ID to response header + w.Header().Set(constants.RequestIDHeader, requestID) + + // Add request ID to context + ctx := context.WithValue(r.Context(), constants.RequestIDContextID, requestID) + + // Log the request ID using the context-aware logger + // So using slog along with the context + // This allows the request ID to be included in all logs for this request + ctx = log.AppendCtx(ctx, slog.String(constants.RequestIDHeader, requestID)) + + // Create a new request with the updated context + r = r.WithContext(ctx) + + // Call the next handler + next.ServeHTTP(w, r) + }) + } +} + +// generateRequestID generates a new unique request ID +func generateRequestID() string { + return uuid.New().String() +} diff --git a/internal/middleware/request_id_test.go b/internal/middleware/request_id_test.go new file mode 100644 index 0000000..4f08e41 --- /dev/null +++ b/internal/middleware/request_id_test.go @@ -0,0 +1,167 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +package middleware + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/linuxfoundation/lfx-v2-project-service/pkg/constants" + "github.com/stretchr/testify/assert" +) + +func TestRequestIDMiddleware(t *testing.T) { + tests := []struct { + name string + existingRequestID string + expectGenerated bool + expectHeaderSet bool + }{ + { + name: "generates new request ID when none provided", + existingRequestID: "", + expectGenerated: true, + expectHeaderSet: true, + }, + { + name: "uses existing request ID when provided", + existingRequestID: "existing-id-123", + expectGenerated: false, + expectHeaderSet: true, + }, + { + name: "uses existing request ID with UUID format", + existingRequestID: "550e8400-e29b-41d4-a716-446655440000", + expectGenerated: false, + expectHeaderSet: true, + }, + } + + assertion := assert.New(t) + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var capturedRequestID string + var capturedContext context.Context + + // Test handler that captures the request ID and context + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + capturedRequestID = getRequestIDFromContext(r.Context()) + capturedContext = r.Context() + w.WriteHeader(http.StatusOK) + }) + + // Wrap handler with RequestID middleware + middleware := RequestIDMiddleware() + wrappedHandler := middleware(handler) + + // Create request + req := httptest.NewRequest("GET", "/test", nil) + if tc.existingRequestID != "" { + req.Header.Set(constants.RequestIDHeader, tc.existingRequestID) + } + + rec := httptest.NewRecorder() + wrappedHandler.ServeHTTP(rec, req) + + // Verify request ID was captured + assertion.NotEmpty(capturedRequestID) + + // Verify request ID matches expectation + if tc.expectGenerated { + // Should be a UUID format (36 characters with dashes) + assertion.Equal(36, len(capturedRequestID)) + assertion.Contains(capturedRequestID, "-") + } else { + assertion.Equal(tc.existingRequestID, capturedRequestID) + } + + // Verify response header contains request ID + if tc.expectHeaderSet { + responseRequestID := rec.Header().Get(constants.RequestIDHeader) + assertion.Equal(capturedRequestID, responseRequestID) + } + + // Verify context contains request ID + contextRequestID := getRequestIDFromContext(capturedContext) + assertion.Equal(capturedRequestID, contextRequestID) + }) + } +} + +func TestMiddlewareIntegration(t *testing.T) { + tests := []struct { + name string + numRequests int + expectUnique bool + }{ + { + name: "generates different request IDs for multiple requests", + numRequests: 3, + expectUnique: true, + }, + { + name: "handles single request correctly", + numRequests: 1, + expectUnique: true, + }, + { + name: "handles many requests correctly", + numRequests: 5, + expectUnique: true, + }, + } + + assertion := assert.New(t) + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var requestIDs []string + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + requestID := getRequestIDFromContext(r.Context()) + requestIDs = append(requestIDs, requestID) + w.WriteHeader(http.StatusOK) + }) + + middleware := RequestIDMiddleware() + wrappedHandler := middleware(handler) + + // Make multiple requests + for i := 0; i < tc.numRequests; i++ { + req := httptest.NewRequest("GET", "/test", nil) + rec := httptest.NewRecorder() + wrappedHandler.ServeHTTP(rec, req) + } + + // Verify we got the expected number of request IDs + assertion.Equal(tc.numRequests, len(requestIDs)) + + // Verify uniqueness if expected + if tc.expectUnique && tc.numRequests > 1 { + uniqueIDs := make(map[string]bool) + for _, id := range requestIDs { + assertion.False(uniqueIDs[id], "Found duplicate request ID: %s", id) + uniqueIDs[id] = true + } + } + + // Verify all IDs are non-empty and properly formatted + for _, id := range requestIDs { + assertion.NotEmpty(id) + assertion.Equal(36, len(id)) + } + }) + } +} + +// Helper function to extract request ID from context +func getRequestIDFromContext(ctx context.Context) string { + if requestID, ok := ctx.Value(constants.RequestIDContextID).(string); ok { + return requestID + } + return "" +} diff --git a/internal/middleware/request_logger.go b/internal/middleware/request_logger.go new file mode 100644 index 0000000..febad01 --- /dev/null +++ b/internal/middleware/request_logger.go @@ -0,0 +1,67 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +package middleware + +import ( + "log/slog" + "net/http" + "time" + + "github.com/linuxfoundation/lfx-v2-project-service/internal/log" + "github.com/linuxfoundation/lfx-v2-project-service/pkg/constants" +) + +// RequestLoggerMiddleware creates a middleware that logs HTTP requests +func RequestLoggerMiddleware() func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + + // Add request URL attributes to the context so that they can be used in all request handler logs + ctx := r.Context() + ctx = log.AppendCtx(ctx, slog.String("method", r.Method)) + ctx = log.AppendCtx(ctx, slog.String("path", r.URL.Path)) + ctx = log.AppendCtx(ctx, slog.String("query", r.URL.RawQuery)) + ctx = log.AppendCtx(ctx, slog.String("host", r.Host)) + ctx = log.AppendCtx(ctx, slog.String("user_agent", r.UserAgent())) + ctx = log.AppendCtx(ctx, slog.String("remote_addr", r.RemoteAddr)) + + if r.Header.Get(constants.EtagHeader) != "" { + ctx = log.AppendCtx(ctx, slog.String("req_header_etag", r.Header.Get(constants.EtagHeader))) + } + + // Create a new request with the updated context + r = r.WithContext(ctx) + + // Create a response writer wrapper to capture status code + ww := &responseWriter{ResponseWriter: w} + + slog.InfoContext(ctx, "HTTP request") + + // Call the next handler + next.ServeHTTP(ww, r) + + // Calculate duration + duration := time.Since(start) + + // Log the response + slog.InfoContext(ctx, "HTTP response", "status", ww.statusCode, "duration", duration.String()) + }) + } +} + +// responseWriter wraps http.ResponseWriter to capture status code +type responseWriter struct { + http.ResponseWriter + statusCode int +} + +func (rw *responseWriter) WriteHeader(code int) { + rw.statusCode = code + rw.ResponseWriter.WriteHeader(code) +} + +func (rw *responseWriter) Write(b []byte) (int, error) { + return rw.ResponseWriter.Write(b) +} diff --git a/internal/middleware/request_logger_test.go b/internal/middleware/request_logger_test.go new file mode 100644 index 0000000..6231844 --- /dev/null +++ b/internal/middleware/request_logger_test.go @@ -0,0 +1,108 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +package middleware + +import ( + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestRequestLoggerMiddleware(t *testing.T) { + tests := []struct { + name string + method string + path string + expectedStatus int + userAgent string + remoteAddr string + }{ + { + name: "logs GET request correctly", + method: "GET", + path: "/projects", + expectedStatus: http.StatusOK, + userAgent: "test-agent", + remoteAddr: "127.0.0.1:12345", + }, + { + name: "logs POST request correctly", + method: "POST", + path: "/projects", + expectedStatus: http.StatusCreated, + userAgent: "curl/7.68.0", + remoteAddr: "192.168.1.1:54321", + }, + { + name: "logs error status correctly", + method: "GET", + path: "/nonexistent", + expectedStatus: http.StatusNotFound, + userAgent: "Mozilla/5.0", + remoteAddr: "10.0.0.1:8080", + }, + } + + assertion := assert.New(t) + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Create a test handler that returns the expected status + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Simulate some processing time + time.Sleep(10 * time.Millisecond) + w.WriteHeader(tc.expectedStatus) + w.Write([]byte("test response")) + }) + + // Wrap with RequestLoggerMiddleware + middleware := RequestLoggerMiddleware() + wrappedHandler := middleware(handler) + + // Create request + req := httptest.NewRequest(tc.method, tc.path, nil) + req.Header.Set("User-Agent", tc.userAgent) + req.RemoteAddr = tc.remoteAddr + + rec := httptest.NewRecorder() + + // Record start time + start := time.Now() + wrappedHandler.ServeHTTP(rec, req) + duration := time.Since(start) + + // Verify response + assertion.Equal(tc.expectedStatus, rec.Code) + assertion.Equal("test response", rec.Body.String()) + + // Verify duration is reasonable (should be at least 10ms due to sleep) + assertion.GreaterOrEqual(duration, 10*time.Millisecond) + }) + } +} + +func TestResponseWriterWrapper(t *testing.T) { + assertion := assert.New(t) + + // Create a mock response writer + rec := httptest.NewRecorder() + + // Wrap it with our responseWriter + rw := &responseWriter{ResponseWriter: rec, statusCode: http.StatusOK} + + // Test WriteHeader + rw.WriteHeader(http.StatusNotFound) + assertion.Equal(http.StatusNotFound, rw.statusCode) + assertion.Equal(http.StatusNotFound, rec.Code) + + // Test Write + content := []byte("test content") + n, err := rw.Write(content) + assertion.NoError(err) + assertion.Equal(len(content), n) + assertion.Equal(content, rec.Body.Bytes()) +} diff --git a/pkg/constants/access_control.go b/pkg/constants/access_control.go new file mode 100644 index 0000000..77424e5 --- /dev/null +++ b/pkg/constants/access_control.go @@ -0,0 +1,13 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +// Package constants contains the constants for the project service. +package constants + +const ( + // AccessCheckSubject is the subject used for access control checks + // The subject is of the form: .lfx.access_check.request + AccessCheckSubject = ".lfx.access_check.request" + // AnonymousPrincipal is the identifier for anonymous users + AnonymousPrincipal = `_anonymous` +) diff --git a/pkg/constants/app.go b/pkg/constants/app.go new file mode 100644 index 0000000..fe89f01 --- /dev/null +++ b/pkg/constants/app.go @@ -0,0 +1,10 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +package constants + +// Constants for the application. +const ( + // ErrKey is the key for the error in the context. + ErrKey = "error" +) diff --git a/pkg/constants/http.go b/pkg/constants/http.go new file mode 100644 index 0000000..cb0ee5c --- /dev/null +++ b/pkg/constants/http.go @@ -0,0 +1,39 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +package constants + +// Constants for the HTTP request headers +const ( + // AuthorizationHeader is the header name for the authorization + AuthorizationHeader string = "Authorization" + + // RequestIDHeader is the header name for the request ID + RequestIDHeader string = "X-REQUEST-ID" + + // EtagHeader is the header name for the ETag + EtagHeader string = "ETag" +) + +// contextRequestID is the type for the request ID context key +type contextRequestID string + +// RequestIDContextID is the context ID for the request ID +const RequestIDContextID contextRequestID = "X-REQUEST-ID" + +// contextAuthorization is the type for the authorization context key +type contextAuthorization string + +// AuthorizationContextID is the context ID for the authorization +const AuthorizationContextID contextAuthorization = "authorization" + +// contextPrincipal is the type for the principal context key +type contextPrincipal string + +// PrincipalContextID is the context ID for the principal +const PrincipalContextID contextPrincipal = "x-on-behalf-of" + +type contextEtag string + +// ETagContextID is the context ID for the ETag +const ETagContextID contextEtag = "etag" diff --git a/pkg/constants/lfx.go b/pkg/constants/lfx.go new file mode 100644 index 0000000..cd2eaee --- /dev/null +++ b/pkg/constants/lfx.go @@ -0,0 +1,31 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +package constants + +// LFXEnvironment is the environment name of the LFX platform. +type LFXEnvironment string + +// Constants for the environment names of the LFX platform. +const ( + // LFXEnvironmentDev is the development environment name of the LFX platform. + LFXEnvironmentDev LFXEnvironment = "dev" + // LFXEnvironmentStg is the staging environment name of the LFX platform. + LFXEnvironmentStg LFXEnvironment = "stg" + // LFXEnvironmentProd is the production environment name of the LFX platform. + LFXEnvironmentProd LFXEnvironment = "prod" +) + +// ParseLFXEnvironment parses the LFX environment from a string. +func ParseLFXEnvironment(env string) LFXEnvironment { + switch env { + case "dev", "development": + return LFXEnvironmentDev + case "stg", "stage", "staging": + return LFXEnvironmentStg + case "prod", "production": + return LFXEnvironmentProd + default: + return LFXEnvironmentDev + } +} diff --git a/pkg/constants/nats.go b/pkg/constants/nats.go new file mode 100644 index 0000000..6fe1d97 --- /dev/null +++ b/pkg/constants/nats.go @@ -0,0 +1,42 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +package constants + +// NATS Key-Value store bucket names. +const ( + // KVBucketNameProjects is the name of the KV bucket for projects. + KVBucketNameProjects = "projects" +) + +// NATS subjects that the project service sends messages about. +const ( + // IndexProjectSubject is the subject for the project indexing. + // The subject is of the form: .lfx.index.project + IndexProjectSubject = ".lfx.index.project" + + // UpdateAccessProjectSubject is the subject for the project access control updates. + // The subject is of the form: .lfx.update_access.project + UpdateAccessProjectSubject = ".lfx.update_access.project" + + // DeleteAllAccessSubject is the subject for the project access control deletion. + // The subject is of the form: .lfx.delete_all_access.project + DeleteAllAccessSubject = ".lfx.delete_all_access.project" +) + +// NATS wildcard subjects that the project service handles messages about. +const ( + // ProjectsAPIQueue is the subject name for the projects API. + // The subject is of the form: .lfx.projects-api.queue + ProjectsAPIQueue = ".lfx.projects-api.queue" +) + +// NATS specific subjects that the project service handles messages about. +const ( + // ProjectGetNameSubject is the subject for the project get name. + // The subject is of the form: .lfx.projects-api.get_name + ProjectGetNameSubject = ".lfx.projects-api.get_name" + // ProjectSlugToUIDSubject is the subject for the project slug to UID. + // The subject is of the form: .lfx.projects-api.slug_to_uid + ProjectSlugToUIDSubject = ".lfx.projects-api.slug_to_uid" +) diff --git a/revive.toml b/revive.toml new file mode 100644 index 0000000..39d1a00 --- /dev/null +++ b/revive.toml @@ -0,0 +1,11 @@ +# Copyright The Linux Foundation and each contributor to LFX. +# SPDX-License-Identifier: MIT + +# When set to false, ignores files with "GENERATED" header, similar to golint +ignoreGeneratedHeader = true + +# Disabled rules +# [rule.package-comments] is disabled because it incorrectly flags every package file as +# missing a package comment when you should only need to add a comment to one of the package files. +[rule.package-comments] +Disabled = true diff --git a/scripts/load_mock_data/Makefile b/scripts/load_mock_data/Makefile new file mode 100644 index 0000000..734c369 --- /dev/null +++ b/scripts/load_mock_data/Makefile @@ -0,0 +1,64 @@ +# Copyright The Linux Foundation and each contributor to LFX. +# SPDX-License-Identifier: MIT + +# Makefile for Project Mock Data Loader + +.PHONY: build run clean help test + +# Default target +help: + @echo "Available targets:" + @echo " build - Build the mock data loader binary" + @echo " run - Run the mock data loader (requires -bearer-token)" + @echo " clean - Remove build artifacts" + @echo " test - Run tests (if any)" + @echo "" + @echo "Usage examples:" + @echo " make build" + @echo " make run ARGS='-bearer-token \"your-token\" -num-projects 5'" + @echo " make run ARGS='-bearer-token \"your-token\" -api-url \"http://localhost:8080/projects\"'" + +# Build the binary +build: + @echo "Building mock data loader..." + go build -o bin/load_mock_data main.go + @echo "Binary created: bin/load_mock_data" + +# Run the script +run: + @if [ -z "$(ARGS)" ]; then \ + echo "Error: ARGS variable is required for run target"; \ + echo "Example: make run ARGS='-bearer-token \"your-token\" -num-projects 5'"; \ + exit 1; \ + fi + @if [ ! -f "./bin/load_mock_data" ]; then \ + echo "Binary not found. Building first..."; \ + make build; \ + fi + @echo "Running mock data loader with args: $(ARGS)" + ./bin/load_mock_data $(ARGS) + +# Clean build artifacts +clean: + @echo "Cleaning build artifacts..." + rm -f bin/load_mock_data + @echo "Clean complete" + +# Run tests (placeholder for future tests) +test: + @echo "Running tests..." + @echo "No tests implemented yet" + @echo "Tests complete" + +# Install dependencies (if needed) +deps: + @echo "Installing dependencies..." + go mod tidy + @echo "Dependencies installed" + +# Development mode - build and run with common args +dev: + @echo "Development mode - building and running with sample args..." + make build + @echo "To run with your token:" + @echo " ./bin/load_mock_data -bearer-token \"your-jwt-token\" -num-projects 5" diff --git a/scripts/load_mock_data/README.md b/scripts/load_mock_data/README.md new file mode 100644 index 0000000..68bf17b --- /dev/null +++ b/scripts/load_mock_data/README.md @@ -0,0 +1,162 @@ +# Project Mock Data Loader + +This Go script allows you to insert project mock data via the project service API. It can create any number of projects based on input parameters. + +## Features + +- Generate random project data with realistic names and descriptions +- Configurable number of projects to create +- Proper error handling and logging +- Rate limiting to avoid overwhelming the API +- Support for authentication via Bearer token +- Configurable API endpoint and timeout + +## Prerequisites + +- Go 1.23 or later +- Access to the project service API +- Valid Bearer token for authentication + +## Building the Script + +From the project root directory: + +```bash +go build -o bin/load_mock_data tools/load_mock_data/main.go +``` + +Or run directly: + +```bash +go run tools/load_mock_data/main.go [flags] +``` + +## Usage + +### Basic Usage + +```bash +# Create 10 projects with default settings +./bin/load_mock_data -bearer-token "your-jwt-token-here" + +# Create 5 projects +./bin/load_mock_data -bearer-token "your-jwt-token-here" -num-projects 5 + +# Use a different API endpoint +./bin/load_mock_data -bearer-token "your-jwt-token-here" -api-url "http://api.example.com/projects" +``` + +### Command Line Flags + +| Flag | Description | Default | Required | +|------|-------------|---------|----------| +| `-bearer-token` | JWT Bearer token for authentication | "" | No | +| `-num-projects` | Number of projects to create | 10 | No | +| `-api-url` | Project service API URL | | No | +| `-version` | API version | "1" | No | +| `-timeout` | Request timeout | "30s" | No | + +### Examples + +```bash +# Create 100 projects with custom timeout +./bin/load_mock_data \ + -bearer-token "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \ + -num-projects 100 \ + -timeout 60s + +# Create 5 projects using staging API +./bin/load_mock_data \ + -bearer-token "your-token" \ + -num-projects 5 \ + -api-url "https://staging-api.example.com/projects" + +# Create 25 projects with API version 2 +./bin/load_mock_data \ + -bearer-token "your-token" \ + -num-projects 25 \ + -version "2" +``` + +## Generated Data + +The script generates project data including: + +### Project Names + +- Random selection of 10 hard-coded project names (e.g. Project 1, Project 2) + +### Descriptions + +- Random selection of 10 hard-coded project descriptions (e.g. A test description 1) + +### Auditors and Writers + +- Random selection of 1-3 auditors and writers from a predefined list (e.g. user123, admin001) + +### Slugs + +- Automatically generated from project names +- URL-friendly format following the pattern: `^[a-z][a-z0-9_\-]*[a-z0-9]$` +- Ensures uniqueness by adding index numbers when needed + +## Output + +The script provides detailed logging of the creation process: + +```text +2024/01/15 10:30:00 Starting to create 10 projects... +2024/01/15 10:30:01 Creating project 1/10: Project 1 (project-1) +2024/01/15 10:30:01 Successfully created project: project-1 +2024/01/15 10:30:01 Creating project 2/10: Project 2 (project-2) +2024/01/15 10:30:02 Successfully created project: project-2 +... +2024/01/15 10:30:10 Completed! Successfully created 10 projects, 0 errors +2024/01/15 10:30:10 Mock data loading completed successfully! +``` + +## Error Handling + +The script handles various error scenarios: + +- **Authentication errors**: Invalid or missing Bearer token +- **API errors**: Network issues, server errors, validation errors +- **Rate limiting**: Built-in delays between requests +- **Duplicate slugs**: Automatic index addition to ensure uniqueness + +## Rate Limiting + +To avoid overwhelming the API, the script includes a 100ms delay between project creation requests. This can be adjusted by modifying the `time.Sleep(100 * time.Millisecond)` line in the code. + +## Security Notes + +- Store your Bearer token securely +- Consider using environment variables for sensitive data +- The script does not store or log the Bearer token +- Use HTTPS endpoints in production environments + +## Troubleshooting + +### Common Issues + +1. **Authentication Error**: Ensure your Bearer token is valid and not expired +2. **Network Error**: Check if the API endpoint is accessible +3. **Validation Error**: The script generates valid data, but API validation rules may change +4. **Timeout Error**: Increase the timeout value for large numbers of projects + +### Debug Mode + +To see more detailed information, you can modify the script to include debug logging or use Go's built-in logging flags: + +```bash +go run -v tools/load_mock_data/main.go -bearer-token "your-token" +``` + +## Contributing + +When modifying the script: + +1. Update the project names, descriptions, and manager IDs as needed +2. Test with small numbers of projects first +3. Ensure the slug generation logic follows the API's validation rules +4. Update this README if adding new features or changing behavior diff --git a/scripts/load_mock_data/main.go b/scripts/load_mock_data/main.go new file mode 100644 index 0000000..79c9e70 --- /dev/null +++ b/scripts/load_mock_data/main.go @@ -0,0 +1,321 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +// Package main is the main package for the load_mock_data tool. +// The tool loads mock data into the project service API. +package main + +import ( + "bytes" + "context" + "crypto/rand" + "encoding/json" + "flag" + "fmt" + "log" + "math/big" + "net/http" + "strings" + "time" + + projectservice "github.com/linuxfoundation/lfx-v2-project-service/cmd/project-api/gen/project_service" +) + +// ProjectData represents the structure for creating a project +type ProjectData struct { + Slug string `json:"slug"` + Name string `json:"name"` + Description string `json:"description"` + Public bool `json:"public"` + ParentUID string `json:"parent_uid"` + Auditors []string `json:"auditors"` + Writers []string `json:"writers"` +} + +// ProjectResponse represents the response from the API +type ProjectResponse struct { + ID *string `json:"id,omitempty"` + Slug *string `json:"slug,omitempty"` + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + Public *bool `json:"public,omitempty"` + ParentUID *string `json:"parent_uid,omitempty"` + Auditors []string `json:"auditors,omitempty"` + Writers []string `json:"writers,omitempty"` +} + +// Config holds the configuration for the script +type Config struct { + APIURL string + BearerToken string + NumProjects int + Version string + Timeout time.Duration +} + +// ProjectGenerator generates random project data +type ProjectGenerator struct { + descriptions []string + managerIDs []string +} + +// NewProjectGenerator creates a new project generator with predefined data +func NewProjectGenerator() *ProjectGenerator { + return &ProjectGenerator{ + descriptions: []string{ + "A test description 1", + "A test description 2", + "A test description 3", + "A test description 4", + "A test description 5", + "A test description 6", + "A test description 7", + "A test description 8", + "A test description 9", + "A test description 10", + }, + managerIDs: []string{ + "user123", "user456", "user789", "admin001", "admin002", "manager001", + "manager002", "lead001", "lead002", "pm001", "pm002", "tech_lead_001", + }, + } +} + +// GenerateProject creates a random project with the given index +func (pg *ProjectGenerator) GenerateProject(index int) ProjectData { + // Generate a random project number (1 to 100000) + projectNumber, _ := rand.Int(rand.Reader, big.NewInt(100000)) + projectNumber.Add(projectNumber, big.NewInt(1)) // Add 1 to make range 1-100000 + + // Generate a random description + descIndex, _ := rand.Int(rand.Reader, big.NewInt(int64(len(pg.descriptions)))) + + name := fmt.Sprintf("Project %d", projectNumber.Int64()) + description := pg.descriptions[descIndex.Int64()] + + // Generate slug from name + slug := generateSlug(name) + + // Generate random public flag + publicInt, _ := rand.Int(rand.Reader, big.NewInt(2)) + publicInt.Add(publicInt, big.NewInt(1)) + public := publicInt.Int64() == 1 + + // Generate random auditors (1-3 auditors) + numAuditors, _ := rand.Int(rand.Reader, big.NewInt(3)) + numAuditors.Add(numAuditors, big.NewInt(1)) + + auditors := make([]string, numAuditors.Int64()) + for i := range auditors { + auditorIndex, _ := rand.Int(rand.Reader, big.NewInt(int64(len(pg.managerIDs)))) + auditors[i] = pg.managerIDs[auditorIndex.Int64()] + } + + // Generate random writers (1-3 writers) + numWriters, _ := rand.Int(rand.Reader, big.NewInt(3)) + numWriters.Add(numWriters, big.NewInt(1)) + + writers := make([]string, numWriters.Int64()) + for i := range writers { + writerIndex, _ := rand.Int(rand.Reader, big.NewInt(int64(len(pg.managerIDs)))) + writers[i] = pg.managerIDs[writerIndex.Int64()] + } + + return ProjectData{ + Slug: slug, + Name: name, + Description: description, + Public: public, + ParentUID: "", + Auditors: auditors, + Writers: writers, + } +} + +// generateSlug creates a URL-friendly slug from a name +func generateSlug(name string) string { + // Convert to lowercase and replace spaces/special chars with hyphens + slug := strings.ToLower(name) + slug = strings.ReplaceAll(slug, " ", "-") + slug = strings.ReplaceAll(slug, "_", "-") + slug = strings.ReplaceAll(slug, ".", "") + slug = strings.ReplaceAll(slug, ",", "") + slug = strings.ReplaceAll(slug, "(", "") + slug = strings.ReplaceAll(slug, ")", "") + + // Remove multiple consecutive hyphens + for strings.Contains(slug, "--") { + slug = strings.ReplaceAll(slug, "--", "-") + } + + // Remove leading/trailing hyphens + slug = strings.Trim(slug, "-") + + // Ensure it starts with a letter and add index if needed + if len(slug) == 0 || !isLetter(slug[0]) { + slug = "project-" + slug + } + + return slug +} + +// isLetter checks if a byte is a letter +func isLetter(b byte) bool { + return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') +} + +// ProjectClient handles API communication +type ProjectClient struct { + config *Config + client *http.Client +} + +// NewProjectClient creates a new project client +func NewProjectClient(config *Config) *ProjectClient { + return &ProjectClient{ + config: config, + client: &http.Client{ + Timeout: config.Timeout, + }, + } +} + +// CreateProject sends a project creation request to the API +func (pc *ProjectClient) CreateProject(ctx context.Context, project ProjectData) (*ProjectResponse, error) { + // Create the payload + payload := &projectservice.CreateProjectPayload{ + Slug: project.Slug, + Name: project.Name, + Description: project.Description, + Public: &project.Public, + ParentUID: &project.ParentUID, + Auditors: project.Auditors, + Writers: project.Writers, + } + + // Marshal the payload + payloadBytes, err := json.Marshal(map[string]interface{}{ + "slug": payload.Slug, + "name": payload.Name, + "description": payload.Description, + "public": payload.Public, + "parent_uid": payload.ParentUID, + "auditors": payload.Auditors, + "writers": payload.Writers, + }) + if err != nil { + return nil, fmt.Errorf("failed to marshal payload: %w", err) + } + + // Create the request + req, err := http.NewRequestWithContext(ctx, "POST", pc.config.APIURL, bytes.NewBuffer(payloadBytes)) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + // Set headers + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+pc.config.BearerToken) + + // Add version query parameter + q := req.URL.Query() + q.Add("v", pc.config.Version) + req.URL.RawQuery = q.Encode() + + // Send the request + resp, err := pc.client.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to send request: %w", err) + } + defer resp.Body.Close() + + // Check response status + if resp.StatusCode != http.StatusOK { + var errorResp map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&errorResp); err != nil { + return nil, fmt.Errorf("request failed with status %d", resp.StatusCode) + } + return nil, fmt.Errorf("request failed with status %d: %v", resp.StatusCode, errorResp) + } + + // Parse response + var projectResp ProjectResponse + if err := json.NewDecoder(resp.Body).Decode(&projectResp); err != nil { + return nil, fmt.Errorf("failed to decode response: %w", err) + } + + return &projectResp, nil +} + +// LoadMockData loads the specified number of projects +func (pc *ProjectClient) LoadMockData(ctx context.Context, numProjects int) error { + generator := NewProjectGenerator() + + log.Printf("Starting to create %d projects...", numProjects) + + successCount := 0 + errorCount := 0 + + for i := 0; i < numProjects; i++ { + project := generator.GenerateProject(i) + + log.Printf("Creating project %d/%d: %s (%s)", i+1, numProjects, project.Name, project.Slug) + + _, err := pc.CreateProject(ctx, project) + if err != nil { + log.Printf("Error creating project %s: %v", project.Slug, err) + errorCount++ + continue + } + + log.Printf("Successfully created project: %s", project.Slug) + successCount++ + + // Add a small delay to avoid overwhelming the API + time.Sleep(100 * time.Millisecond) + } + + log.Printf("Completed! Successfully created %d projects, %d errors", successCount, errorCount) + return nil +} + +func main() { + // Parse command line flags + var ( + apiURL = flag.String("api-url", "http://localhost:8080/projects", "Project service API URL") + bearerToken = flag.String("bearer-token", "", "Bearer token for authentication") + numProjects = flag.Int("num-projects", 10, "Number of projects to create") + version = flag.String("version", "1", "API version") + timeout = flag.Duration("timeout", 30*time.Second, "Request timeout") + ) + flag.Parse() + + // Validate required parameters + if *numProjects <= 0 { + log.Fatal("Number of projects must be greater than 0.") + } + + // Create configuration + config := &Config{ + APIURL: *apiURL, + BearerToken: *bearerToken, + NumProjects: *numProjects, + Version: *version, + Timeout: *timeout, + } + + // Create client + client := NewProjectClient(config) + + // Create context with timeout + ctx, cancel := context.WithTimeout(context.Background(), config.Timeout*time.Duration(config.NumProjects)) + defer cancel() + + // Load mock data + if err := client.LoadMockData(ctx, config.NumProjects); err != nil { + log.Printf("Failed to load mock data: %v", err) + return + } + + log.Println("Mock data loading completed successfully!") +}