├── .bumpversion.toml ├── .cra └── .cveignore ├── .github └── workflows │ └── build.yaml ├── .gitignore ├── .golangci.yaml ├── .npmrc ├── .releaserc ├── .secrets.baseline ├── Authentication.md ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── core ├── authentication_error.go ├── authentication_error_test.go ├── authenticator.go ├── authenticator_factory.go ├── authenticator_factory_test.go ├── base_service.go ├── base_service_retries_test.go ├── base_service_test.go ├── basic_authenticator.go ├── basic_authenticator_test.go ├── bearer_token_authenticator.go ├── bearer_token_authenticator_test.go ├── common_test.go ├── config_utils.go ├── config_utils_test.go ├── constants.go ├── container_authenticator.go ├── container_authenticator_test.go ├── core_suite_test.go ├── cp4d_authenticator.go ├── cp4d_authenticator_test.go ├── datetime.go ├── datetime_test.go ├── detailed_response.go ├── detailed_response_test.go ├── doc.go ├── file_with_metadata.go ├── file_with_metadata_test.go ├── gzip.go ├── gzip_test.go ├── http_problem.go ├── http_problem_test.go ├── iam_assume_authenticator.go ├── iam_assume_authenticator_test.go ├── iam_authenticator.go ├── iam_authenticator_test.go ├── ibm_problem.go ├── ibm_problem_test.go ├── jwt_utils.go ├── jwt_utils_test.go ├── log.go ├── log_test.go ├── marshal_nulls_test.go ├── mcsp_v1_authenticator.go ├── mcsp_v1_authenticator_test.go ├── mcsp_v2_authenticator.go ├── mcsp_v2_authenticator_test.go ├── noauth_authenticator.go ├── noauth_authenticator_test.go ├── ordered_maps.go ├── ordered_maps_test.go ├── parameterized_url.go ├── parameterized_url_test.go ├── problem.go ├── problem_utils.go ├── problem_utils_test.go ├── request_builder.go ├── request_builder_test.go ├── sdk_problem.go ├── sdk_problem_test.go ├── sdk_problem_utils.go ├── unmarshal_v2.go ├── unmarshal_v2_models_test.go ├── unmarshal_v2_primitives_test.go ├── utils.go ├── utils_test.go ├── version.go ├── vpc_instance_authenticator.go └── vpc_instance_authenticator_test.go ├── go.mod ├── go.sum ├── package.json └── resources ├── cr-token.txt ├── empty-cr-token.txt ├── ibm-credentials.env ├── my-credentials.env ├── test_file.txt └── vcap_services.json /.bumpversion.toml: -------------------------------------------------------------------------------- 1 | [tool.bumpversion] 2 | current_version = "5.20.0" 3 | commit = true 4 | message = "Update version {current_version} -> {new_version} [skip ci]" 5 | 6 | [[tool.bumpversion.files]] 7 | filename = "core/version.go" 8 | search = "__VERSION__ = \"{current_version}\"" 9 | replace = "__VERSION__ = \"{new_version}\"" 10 | 11 | [[tool.bumpversion.files]] 12 | filename = "README.md" 13 | search = "{current_version}" 14 | replace = "{new_version}" 15 | 16 | [[tool.bumpversion.files]] 17 | filename = "Authentication.md" 18 | parse = "(?P\\d+)" 19 | serialize = ["{major}"] 20 | search = "v{current_version}/core" 21 | replace = "v{new_version}/core" 22 | -------------------------------------------------------------------------------- /.cra/.cveignore: -------------------------------------------------------------------------------- 1 | [ 2 | ] -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | # This workflow will build and unit test the project. 2 | # If the workflow is running on the "main" branch, then 3 | # semantic-release is also run to create a new release (if 4 | # warranted by the new commits being built). 5 | 6 | name: build 7 | 8 | on: 9 | push: 10 | branches: ['**'] 11 | pull_request: 12 | branches: ['**'] 13 | workflow_dispatch: 14 | # Allow workflow to be triggered manually. 15 | 16 | jobs: 17 | detect-secrets: 18 | if: "!contains(github.event.head_commit.message, '[skip ci]')" 19 | name: detect-secrets 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - name: Checkout repository 24 | uses: actions/checkout@v4 25 | 26 | - name: Setup Python 27 | uses: actions/setup-python@v5 28 | with: 29 | python-version: 3.12 30 | 31 | - name: Install detect-secrets 32 | run: | 33 | pip install --upgrade "git+https://github.com/ibm/detect-secrets.git@master#egg=detect-secrets" 34 | 35 | - name: Run detect-secrets 36 | run: | 37 | detect-secrets scan --update .secrets.baseline 38 | detect-secrets -v audit --report --fail-on-unaudited --fail-on-live --fail-on-audited-real .secrets.baseline 39 | 40 | build: 41 | name: build-test (go v${{ matrix.go-version }}) 42 | needs: detect-secrets 43 | runs-on: ubuntu-latest 44 | strategy: 45 | matrix: 46 | go-version: ['1.23', '1.24'] 47 | 48 | steps: 49 | - name: Checkout repository 50 | uses: actions/checkout@v4 51 | 52 | - name: Setup Go v${{ matrix.go-version }} 53 | uses: actions/setup-go@v5 54 | with: 55 | go-version: ${{ matrix.go-version }} 56 | 57 | - name: Install dependencies 58 | run: | 59 | curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s -- -b $(go env GOPATH)/bin v1.64.4 60 | golangci-lint version 61 | go install golang.org/x/tools/cmd/goimports@latest 62 | 63 | - name: Build & Test 64 | run: make all 65 | 66 | publish-release: 67 | name: semantic-release 68 | needs: build 69 | if: "github.ref_name == 'main' && github.event_name != 'pull_request'" 70 | runs-on: ubuntu-latest 71 | 72 | steps: 73 | - name: Checkout repository 74 | uses: actions/checkout@v4 75 | with: 76 | persist-credentials: false 77 | 78 | - name: Setup Node.js 79 | uses: actions/setup-node@v4 80 | with: 81 | node-version: 22 82 | 83 | - name: Setup Python 84 | uses: actions/setup-python@v5 85 | with: 86 | python-version: 3.12 87 | 88 | - name: Install Publishing Tools 89 | run: | 90 | pip install bump-my-version 91 | npm install 92 | 93 | - name: Run semantic-release 94 | env: 95 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 96 | run: npm run semantic-release 97 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .ibm-project 3 | .DS_Store 4 | credentials.txt 5 | *.coverprofile 6 | .project 7 | *.env 8 | 9 | # ignore vendor/ 10 | vendor/ 11 | 12 | # coverage 13 | coverage.txt 14 | 15 | # ignore detect secrets files 16 | .pre-commit-config.yaml 17 | .secrets.baseline 18 | /.settings/ 19 | /.vscode/ 20 | 21 | # files produced by "npm install" commands during build 22 | package-lock.json 23 | node_modules/ 24 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | linters: 2 | enable: 3 | - gosec 4 | 5 | run: 6 | timeout: 2m 7 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "branches": "main", 3 | "debug": true, 4 | "plugins": [ 5 | "@semantic-release/commit-analyzer", 6 | "@semantic-release/release-notes-generator", 7 | "@semantic-release/changelog", 8 | [ 9 | "@semantic-release/exec", 10 | { 11 | "prepareCmd": "bump-my-version bump --allow-dirty --current-version ${lastRelease.version} --new-version ${nextRelease.version}" 12 | } 13 | ], 14 | [ 15 | "@semantic-release/git", 16 | { 17 | "assets" : [ "CHANGELOG.md" ], 18 | "message": "chore(release): ${nextRelease.version} release notes [skip ci]\n\n${nextRelease.notes}" 19 | } 20 | ], 21 | "@semantic-release/github" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Issues 2 | 3 | If you encounter an issue with the SDK, you are welcome to submit a [bug report](https://github.com/IBM/go-sdk-core/issues). 4 | Before that, please search for similar issues. It's possible somebody has encountered this issue already. 5 | 6 | # Code 7 | ## Commit Messages 8 | Commit messages should follow the [Angular Commit Message Guidelines](https://github.com/angular/angular/blob/master/CONTRIBUTING.md#-commit-message-guidelines). 9 | This is because our release tool - [semantic-release](https://github.com/semantic-release/semantic-release) - 10 | uses this format for determining release versions and generating changelogs. 11 | Tools such as [commitizen](https://github.com/commitizen/cz-cli) or [commitlint](https://github.com/conventional-changelog/commitlint) 12 | can be used to help contributors and enforce commit messages. 13 | Here are some examples of acceptable commit messages, along with the release type that would be done based on the commit message: 14 | 15 | | Commit message | Release type | 16 | |-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------| 17 | | `fix(IAM Authentication) propagate token request errors back to request invocation thread` | Patch Release | 18 | | `feat(JSON Serialization): add custom deserializer for dynamic models` | ~~Minor~~ Feature Release | 19 | | `feat(BaseService): added baseURL as param to BaseService ctor`

`BREAKING CHANGE: The global-search service has been updated to reflect version 3 of the API.` | ~~Major~~ Breaking Release | 20 | 21 | # Pull Requests 22 | 23 | If you want to contribute to the repository, here's a quick guide: 24 | 1. Fork the repository 25 | - If you have "write" access to the repository, you can avoid using a fork. 26 | 27 | 2. The `go-sdk-core` project uses Go modules for dependency management, so do NOT set the `GOPATH` environment 28 | variable to include your local `go-sdk-core` project directory. 29 | 30 | 3. Clone the respository into a local directory. 31 | 32 | 4. Install the `golangci-lint` tool: 33 | ```sh 34 | curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s -- -b $(go env GOPATH)/bin v1.51.2 35 | ``` 36 | * Note: As of this writing, the 1.51.2 version of `golangci-lint` is being used by this project. 37 | Please check the `curl` command found in the `.travis.yml` file to see the version of this tool that is currently 38 | being used at the time you are planning to commit changes. This will ensure that you are using the same version 39 | of the linter as the Travis build automation, which will ensure that you are using the same set of linter checks 40 | that the automated build uses. 41 | 42 | 5. Install the `gosec` tool: 43 | ```sh 44 | curl -sfL https://raw.githubusercontent.com/securego/gosec/master/install.sh | sh -s -- -b $(go env GOPATH)/bin 45 | ``` 46 | 47 | 6. Make sure that you have a clean "baseline" to work from by building/testing the project before 48 | you start to make changes: 49 | ```sh 50 | make all 51 | ``` 52 | 53 | 7. Make your code changes as needed. Be sure to add new tests for any new or modified functionality. 54 | 55 | 8. Test your changes: 56 | ```sh 57 | make test 58 | ``` 59 | The above command will run all the unit tests with the command `go test -tags=all`. 60 | Each unit test file contains one or more build tags as a way to classify the 61 | tests into various groups (example: `//go:build all || fast || auth`). 62 | Currently, these tags include: all, slow, fast, auth, basesvc, log and retries. 63 | Others might be added in the future. 64 | To run a specific class of tests (example 'retries'), use a command like this: 65 | ``` 66 | cd core 67 | go test -tags=retries 68 | ``` 69 | 70 | 9. Check your code for lint issues: 71 | ```sh 72 | make lint 73 | ``` 74 | 75 | 10. To build, test and lint check in one step: 76 | ```sh 77 | make all 78 | ``` 79 | 80 | 11. Make sure there are no security vulnerabilities by running `gosec`: 81 | ```sh 82 | make scan-gosec 83 | ``` 84 | 85 | 12. Commit your changes: 86 | * Commit messages should follow the Angular commit message guidelines as mentioned above. 87 | 88 | 13. Push your branch to remote and submit a pull request to the **main** branch. 89 | 90 | # Developer's Certificate of Origin 1.1 91 | 92 | By making a contribution to this project, I certify that: 93 | 94 | (a) The contribution was created in whole or in part by me and I 95 | have the right to submit it under the open source license 96 | indicated in the file; or 97 | 98 | (b) The contribution is based upon previous work that, to the best 99 | of my knowledge, is covered under an appropriate open source 100 | license and I have the right under that license to submit that 101 | work with modifications, whether created in whole or in part 102 | by me, under the same open source license (unless I am 103 | permitted to submit under a different license), as indicated 104 | in the file; or 105 | 106 | (c) The contribution was provided directly to me by some other 107 | person who certified (a), (b) or (c) and I have not modified 108 | it. 109 | 110 | (d) I understand and agree that this project and the contribution 111 | are public and that a record of the contribution (including all 112 | personal information I submit with it, including my sign-off) is 113 | maintained indefinitely and may be redistributed consistent with 114 | this project or the open source license(s) involved. 115 | 116 | # Additional Resources 117 | + [General GitHub documentation](https://help.github.com/) 118 | + [GitHub pull request documentation](https://help.github.com/send-pull-requests/) 119 | 120 | [dw]: https://developer.ibm.com/answers/questions/ask.html 121 | [stackoverflow]: http://stackoverflow.com/questions/ask?tags=ibm 122 | [dep]: https://github.com/golang/dep 123 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Makefile to build go-sdk-core library 2 | GO=go 3 | LINT=golangci-lint 4 | FORMATTER=goimports 5 | 6 | COV_OPTS=-coverprofile=coverage.txt -covermode=atomic 7 | 8 | all: tidy test lint 9 | 10 | build: 11 | ${GO} build ./... 12 | 13 | testcov: 14 | ${GO} test -tags=all ${COV_OPTS} ./... 15 | 16 | test: 17 | ${GO} test -tags=all ./... 18 | 19 | lint: 20 | ${LINT} run --build-tags=all 21 | DIFF=$$(${FORMATTER} -d core); if [ -n "$$DIFF" ]; then printf "\n$$DIFF\n" && exit 1; fi 22 | 23 | format: 24 | ${FORMATTER} -w core 25 | 26 | tidy: 27 | ${GO} mod tidy 28 | 29 | detect-secrets: 30 | detect-secrets scan --update .secrets.baseline 31 | detect-secrets audit .secrets.baseline 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://github.com/IBM/go-sdk-core/actions/workflows/build.yaml/badge.svg)](https://github.com/IBM/go-sdk-core/actions/workflows/build.yaml) 2 | [![Release](https://img.shields.io/github/v/release/IBM/go-sdk-core)](https://github.com/IBM/go-sdk-core/releases/latest) 3 | ![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/IBM/go-sdk-core?filename=go.mod) 4 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 5 | [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) 6 | [![CLA assistant](https://cla-assistant.io/readme/badge/ibm/go-sdk-core)](https://cla-assistant.io/ibm/go-sdk-core) 7 | 8 | 9 | # IBM Go SDK Core Version 5.20.0 10 | This project contains core functionality required by Go code generated by the IBM Cloud OpenAPI SDK Generator 11 | (openapi-sdkgen). 12 | 13 | ## Installation 14 | 15 | Get SDK package: 16 | ```bash 17 | go get -u github.com/IBM/go-sdk-core/... 18 | ``` 19 | 20 | ## Prerequisites 21 | - Go version 1.23 or newer 22 | 23 | ## Authentication 24 | The go-sdk-core project supports the following types of authentication: 25 | - Basic Authentication 26 | - Bearer Token Authentication 27 | - Identity and Access Management (IAM) Authentication (grant type: apikey) 28 | - Identity and Access Management (IAM) Authentication (grant type: assume) 29 | - Container Authentication 30 | - VPC Instance Authentication 31 | - Cloud Pak for Data Authentication 32 | - Multi-Cloud Saas Platform (MCSP) Authentication 33 | - No Authentication (for testing) 34 | 35 | For more information about the various authentication types and how to use them with your services, click [here](Authentication.md). 36 | 37 | ## Logging 38 | The go-sdk-core project implements a basic logging facility to log various messages. 39 | The logger supports these logging levels: Error, Info, Warn, and Debug. 40 | 41 | By default, the project will use a logger with log level "Error" configured, which means that 42 | only error messages will be displayed. A logger configured at log level "Warn" would display "Error" and "Warn" messages 43 | (but not "Info" or "Debug"), etc. 44 | 45 | To configure the logger to display "Info", "Warn" and "Error" messages, use the `core.SetLoggingLevel()` 46 | method, as in this example: 47 | 48 | ```go 49 | import ( 50 | "github.com/IBM/go-sdk-core/v5/core" 51 | ) 52 | 53 | // Enable Info logging. 54 | core.SetLoggingLevel(core.LevelInfo) 55 | ``` 56 | 57 | If you configure the logger for log level "Debug", then HTTP request/response messages will be logged as well. 58 | Here is an example that shows this, along with the steps needed to enable automatic retries: 59 | 60 | ```go 61 | // Enable Debug logging. 62 | core.SetLoggingLevel(core.LevelDebug) 63 | 64 | // Construct the service client. 65 | myService, err := exampleservicev1.NewExampleServiceV1(options) 66 | 67 | // Enable automatic retries. 68 | myService.EnableRetries(3, 20 * time.Second) 69 | 70 | // Create the resource. 71 | result, detailedResponse, err := myService.CreateResource(createResourceOptionsModel) 72 | ``` 73 | 74 | When the "CreateResource()" method is invoked, you should see a handful of debug messages 75 | displayed on the console reporting on progress of the request, including any retries that 76 | were performed. Here is an example: 77 | 78 | ``` 79 | 2020/10/29 10:34:57 [DEBUG] POST http://example-service.cloud.ibm.com/api/v1/resource 80 | 2020/10/29 10:34:57 [DEBUG] POST http://example-service.cloud.ibm.com/api/v1/resource (status: 429): retrying in 1s (5 left) 81 | 2020/10/29 10:34:58 [DEBUG] POST http://example-service.cloud.ibm.com/api/v1/resource (status: 429): retrying in 1s (4 left) 82 | ``` 83 | 84 | In addition to providing a basic logger implementation, the Go core library also defines 85 | the `Logger` interface and allows users to supply their own implementation to support unique 86 | logging requirements (perhaps you need messages logged to a file instead of the console). 87 | To use this advanced feature, simply implement the `Logger` interface and then call the 88 | `SetLogger(Logger)` function to set your implementation as the logger to be used by the 89 | Go core library. 90 | 91 | ## Issues 92 | 93 | If you encounter an issue with this project, you are welcome to submit a [bug report](https://github.com/IBM/go-sdk-core/issues). 94 | Before opening a new issue, please search for similar issues. It's possible that someone has already reported it. 95 | 96 | ## Tests 97 | 98 | To build, test and lint-check the project: 99 | ```bash 100 | make all 101 | ``` 102 | 103 | Get code coverage for each test suite: 104 | ```bash 105 | go test -coverprofile=coverage.out ./... 106 | go tool cover -html=coverage.out 107 | ``` 108 | 109 | ## Contributing 110 | 111 | See [CONTRIBUTING](CONTRIBUTING.md). 112 | 113 | ## License 114 | 115 | This library is licensed under Apache 2.0. Full license text is 116 | available in [LICENSE](LICENSE). 117 | -------------------------------------------------------------------------------- /core/authentication_error.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | // (C) Copyright IBM Corp. 2024. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | import ( 18 | "errors" 19 | ) 20 | 21 | // AuthenticationError describes the problem returned when 22 | // authentication over HTTP fails. 23 | type AuthenticationError struct { 24 | Err error 25 | *HTTPProblem 26 | } 27 | 28 | // NewAuthenticationError is a deprecated function that was previously used for creating new 29 | // AuthenticationError structs. HTTPProblem types should be used instead of AuthenticationError types. 30 | func NewAuthenticationError(response *DetailedResponse, err error) *AuthenticationError { 31 | GetLogger().Warn("NewAuthenticationError is deprecated and should not be used.") 32 | authError := authenticationErrorf(err, response, "unknown", NewProblemComponent("unknown", "unknown")) 33 | return authError 34 | } 35 | 36 | // authenticationErrorf creates and returns a new instance of "AuthenticationError". 37 | func authenticationErrorf(err error, response *DetailedResponse, operationID string, component *ProblemComponent) *AuthenticationError { 38 | // This function should always be called with non-nil error instances. 39 | if err == nil { 40 | return nil 41 | } 42 | 43 | var httpErr *HTTPProblem 44 | if !errors.As(err, &httpErr) { 45 | if response == nil { 46 | return nil 47 | } 48 | httpErr = httpErrorf(err.Error(), response) 49 | } 50 | 51 | enrichHTTPProblem(httpErr, operationID, component) 52 | 53 | return &AuthenticationError{ 54 | HTTPProblem: httpErr, 55 | Err: err, 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /core/authentication_error_test.go: -------------------------------------------------------------------------------- 1 | //go:build all || slow || auth 2 | 3 | package core 4 | 5 | // (C) Copyright IBM Corp. 2024. 6 | // 7 | // Licensed under the Apache License, Version 2.0 (the "License"); 8 | // you may not use this file except in compliance with the License. 9 | // You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, software 14 | // distributed under the License is distributed on an "AS IS" BASIS, 15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | // See the License for the specific language governing permissions and 17 | // limitations under the License. 18 | 19 | import ( 20 | "errors" 21 | "testing" 22 | 23 | "github.com/stretchr/testify/assert" 24 | ) 25 | 26 | func TestEmbedsHTTPProblem(t *testing.T) { 27 | authErr := &AuthenticationError{ 28 | Err: errors.New(""), 29 | HTTPProblem: &HTTPProblem{ 30 | OperationID: "", 31 | Response: &DetailedResponse{}, 32 | }, 33 | } 34 | 35 | assert.NotNil(t, authErr.GetConsoleMessage) 36 | assert.NotNil(t, authErr.GetDebugMessage) 37 | assert.NotNil(t, authErr.GetID) 38 | assert.NotNil(t, authErr.getErrorCode) 39 | assert.NotNil(t, authErr.getHeader) 40 | assert.NotNil(t, authErr.GetConsoleOrderedMaps) 41 | assert.NotNil(t, authErr.GetDebugOrderedMaps) 42 | } 43 | 44 | func TestNewAuthenticationError(t *testing.T) { 45 | unknown := "unknown" 46 | err := errors.New("test") 47 | resp := getMockAuthResponse() 48 | 49 | authErr := NewAuthenticationError(resp, err) 50 | 51 | assert.NotNil(t, authErr) 52 | assert.Equal(t, err, authErr.Err) 53 | assert.Equal(t, resp, authErr.Response) 54 | assert.Equal(t, "test", authErr.Summary) 55 | assert.Equal(t, unknown, authErr.OperationID) 56 | assert.Equal(t, unknown, authErr.Component.Name) 57 | assert.Equal(t, unknown, authErr.Component.Version) 58 | } 59 | 60 | func TestAuthenticationErrorfHTTPProblem(t *testing.T) { 61 | resp := getMockAuthResponse() 62 | httpProb := httpErrorf("Unauthorized", resp) 63 | assert.Empty(t, httpProb.OperationID) 64 | assert.Empty(t, httpProb.Component) 65 | 66 | authErr := authenticationErrorf(httpProb, nil, "get_token", NewProblemComponent("iam", "v1")) 67 | assert.Equal(t, httpProb, authErr.Err) 68 | assert.Equal(t, resp, authErr.Response) 69 | assert.Equal(t, "Unauthorized", authErr.Summary) 70 | assert.Equal(t, "get_token", authErr.OperationID) 71 | assert.Equal(t, "iam", authErr.Component.Name) 72 | assert.Equal(t, "v1", authErr.Component.Version) 73 | } 74 | 75 | func TestAuthenticationErrorfOtherError(t *testing.T) { 76 | err := errors.New("test") 77 | resp := getMockAuthResponse() 78 | 79 | authErr := authenticationErrorf(err, resp, "get_token", NewProblemComponent("iam", "v1")) 80 | assert.NotNil(t, authErr) 81 | assert.Equal(t, err, authErr.Err) 82 | assert.Equal(t, resp, authErr.Response) 83 | assert.Equal(t, "test", authErr.Summary) 84 | assert.Equal(t, "get_token", authErr.OperationID) 85 | assert.Equal(t, "iam", authErr.Component.Name) 86 | assert.Equal(t, "v1", authErr.Component.Version) 87 | } 88 | 89 | func TestAuthenticationErrorfNoErr(t *testing.T) { 90 | authErr := authenticationErrorf(nil, getMockAuthResponse(), "get_token", NewProblemComponent("iam", "v1")) 91 | assert.Nil(t, authErr) 92 | } 93 | 94 | func TestAuthenticationErrorfNoResponse(t *testing.T) { 95 | authErr := authenticationErrorf(errors.New("not http, needs response"), nil, "get_token", NewProblemComponent("iam", "v1")) 96 | assert.Nil(t, authErr) 97 | } 98 | 99 | func getMockAuthResponse() *DetailedResponse { 100 | return &DetailedResponse{ 101 | StatusCode: 401, 102 | Result: "Unauthorized", 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /core/authenticator.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | // (C) Copyright IBM Corp. 2019. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | import ( 18 | "net/http" 19 | ) 20 | 21 | // Authenticator describes the set of methods implemented by each authenticator. 22 | type Authenticator interface { 23 | AuthenticationType() string 24 | Authenticate(*http.Request) error 25 | Validate() error 26 | } 27 | -------------------------------------------------------------------------------- /core/authenticator_factory.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | // (C) Copyright IBM Corp. 2019, 2025. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | import ( 18 | "fmt" 19 | "strings" 20 | ) 21 | 22 | // GetAuthenticatorFromEnvironment instantiates an Authenticator using service properties 23 | // retrieved from external config sources. 24 | func GetAuthenticatorFromEnvironment(credentialKey string) (authenticator Authenticator, err error) { 25 | GetLogger().Debug("Get authenticator from environment, key=%s\n", credentialKey) 26 | properties, err := getServiceProperties(credentialKey) 27 | if len(properties) == 0 { 28 | return 29 | } 30 | 31 | // Determine the authentication type if not specified explicitly. 32 | authType := properties[PROPNAME_AUTH_TYPE] 33 | 34 | // Support alternate "AUTHTYPE" property. 35 | if authType == "" { 36 | authType = properties["AUTHTYPE"] 37 | } 38 | 39 | // Determine a default auth type if one wasn't specified. 40 | if authType == "" { 41 | // If the APIKEY property is specified, then we'll guess IAM... otherwise CR Auth. 42 | if properties[PROPNAME_APIKEY] != "" { 43 | authType = AUTHTYPE_IAM 44 | } else { 45 | authType = AUTHTYPE_CONTAINER 46 | } 47 | } 48 | 49 | // Create the authenticator appropriate for the auth type. 50 | if strings.EqualFold(authType, AUTHTYPE_BASIC) { 51 | authenticator, err = newBasicAuthenticatorFromMap(properties) 52 | } else if strings.EqualFold(authType, AUTHTYPE_BEARER_TOKEN) { 53 | authenticator, err = newBearerTokenAuthenticatorFromMap(properties) 54 | } else if strings.EqualFold(authType, AUTHTYPE_IAM) { 55 | authenticator, err = newIamAuthenticatorFromMap(properties) 56 | } else if strings.EqualFold(authType, AUTHTYPE_IAM_ASSUME) { 57 | authenticator, err = newIamAssumeAuthenticatorFromMap(properties) 58 | } else if strings.EqualFold(authType, AUTHTYPE_CONTAINER) { 59 | authenticator, err = newContainerAuthenticatorFromMap(properties) 60 | } else if strings.EqualFold(authType, AUTHTYPE_VPC) { 61 | authenticator, err = newVpcInstanceAuthenticatorFromMap(properties) 62 | } else if strings.EqualFold(authType, AUTHTYPE_CP4D) { 63 | authenticator, err = newCloudPakForDataAuthenticatorFromMap(properties) 64 | } else if strings.EqualFold(authType, AUTHTYPE_MCSP) { 65 | authenticator, err = newMCSPAuthenticatorFromMap(properties) 66 | } else if strings.EqualFold(authType, AUTHTYPE_MCSPV2) { 67 | authenticator, err = newMCSPV2AuthenticatorFromMap(properties) 68 | } else if strings.EqualFold(authType, AUTHTYPE_NOAUTH) { 69 | authenticator, err = NewNoAuthAuthenticator() 70 | } else { 71 | err = SDKErrorf( 72 | nil, 73 | fmt.Sprintf(ERRORMSG_AUTHTYPE_UNKNOWN, authType), 74 | "unknown-auth-type", 75 | getComponentInfo(), 76 | ) 77 | } 78 | 79 | if authenticator != nil { 80 | GetLogger().Debug("Returning authenticator, type=%s\n", authenticator.AuthenticationType()) 81 | } 82 | 83 | return 84 | } 85 | -------------------------------------------------------------------------------- /core/basic_authenticator.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | // (C) Copyright IBM Corp. 2019. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | import ( 18 | "errors" 19 | "fmt" 20 | "net/http" 21 | ) 22 | 23 | // BasicAuthenticator takes a user-supplied username and password, and adds 24 | // them to requests via an Authorization header of the form: 25 | // 26 | // Authorization: Basic 27 | type BasicAuthenticator struct { 28 | // Username is the user-supplied basic auth username [required]. 29 | Username string 30 | // Password is the user-supplied basic auth password [required]. 31 | Password string 32 | } 33 | 34 | // NewBasicAuthenticator constructs a new BasicAuthenticator instance. 35 | func NewBasicAuthenticator(username string, password string) (*BasicAuthenticator, error) { 36 | obj := &BasicAuthenticator{ 37 | Username: username, 38 | Password: password, 39 | } 40 | if err := obj.Validate(); err != nil { 41 | err = RepurposeSDKProblem(err, "validation-failed") 42 | return nil, err 43 | } 44 | return obj, nil 45 | } 46 | 47 | // newBasicAuthenticatorFromMap constructs a new BasicAuthenticator instance 48 | // from a map. 49 | func newBasicAuthenticatorFromMap(properties map[string]string) (*BasicAuthenticator, error) { 50 | if properties == nil { 51 | err := errors.New(ERRORMSG_PROPS_MAP_NIL) 52 | return nil, SDKErrorf(err, "", "missing-props", getComponentInfo()) 53 | } 54 | 55 | return NewBasicAuthenticator(properties[PROPNAME_USERNAME], properties[PROPNAME_PASSWORD]) 56 | } 57 | 58 | // AuthenticationType returns the authentication type for this authenticator. 59 | func (BasicAuthenticator) AuthenticationType() string { 60 | return AUTHTYPE_BASIC 61 | } 62 | 63 | // Authenticate adds basic authentication information to a request. 64 | // 65 | // Basic Authorization will be added to the request's headers in the form: 66 | // 67 | // Authorization: Basic 68 | func (authenticator *BasicAuthenticator) Authenticate(request *http.Request) error { 69 | request.SetBasicAuth(authenticator.Username, authenticator.Password) 70 | GetLogger().Debug("Authenticated outbound request (type=%s)\n", authenticator.AuthenticationType()) 71 | return nil 72 | } 73 | 74 | // Validate the authenticator's configuration. 75 | // 76 | // Ensures the username and password are not Nil. Additionally, ensures 77 | // they do not contain invalid characters. 78 | func (authenticator BasicAuthenticator) Validate() error { 79 | if authenticator.Username == "" { 80 | err := fmt.Errorf(ERRORMSG_PROP_MISSING, "Username") 81 | return SDKErrorf(err, "", "no-user", getComponentInfo()) 82 | } 83 | 84 | if authenticator.Password == "" { 85 | err := fmt.Errorf(ERRORMSG_PROP_MISSING, "Password") 86 | return SDKErrorf(err, "", "no-pass", getComponentInfo()) 87 | } 88 | 89 | if HasBadFirstOrLastChar(authenticator.Username) { 90 | err := fmt.Errorf(ERRORMSG_PROP_INVALID, "Username") 91 | return SDKErrorf(err, "", "bad-user", getComponentInfo()) 92 | } 93 | 94 | if HasBadFirstOrLastChar(authenticator.Password) { 95 | err := fmt.Errorf(ERRORMSG_PROP_INVALID, "Password") 96 | return SDKErrorf(err, "", "bad-pass", getComponentInfo()) 97 | } 98 | 99 | return nil 100 | } 101 | -------------------------------------------------------------------------------- /core/basic_authenticator_test.go: -------------------------------------------------------------------------------- 1 | //go:build all || fast || auth 2 | 3 | package core 4 | 5 | // (C) Copyright IBM Corp. 2019. 6 | // 7 | // Licensed under the Apache License, Version 2.0 (the "License"); 8 | // you may not use this file except in compliance with the License. 9 | // You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, software 14 | // distributed under the License is distributed on an "AS IS" BASIS, 15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | // See the License for the specific language governing permissions and 17 | // limitations under the License. 18 | 19 | import ( 20 | "fmt" 21 | "testing" 22 | 23 | "github.com/stretchr/testify/assert" 24 | ) 25 | 26 | var ( 27 | // To enable debug logging during test execution, set this to "LevelDebug" 28 | basicAuthTestLogLevel LogLevel = LevelError 29 | ) 30 | 31 | func TestBasicAuthUsername(t *testing.T) { 32 | authenticator := &BasicAuthenticator{ 33 | Username: "{username}", 34 | Password: "password", 35 | } 36 | err := authenticator.Validate() 37 | assert.NotNil(t, err) 38 | assert.Equal(t, fmt.Errorf(ERRORMSG_PROP_INVALID, "Username").Error(), err.Error()) 39 | 40 | _, err = NewBasicAuthenticator("\"username\"", "password") 41 | assert.NotNil(t, err) 42 | assert.Equal(t, fmt.Errorf(ERRORMSG_PROP_INVALID, "Username").Error(), err.Error()) 43 | 44 | _, err = NewBasicAuthenticator("", "password") 45 | assert.NotNil(t, err) 46 | assert.Equal(t, fmt.Errorf(ERRORMSG_PROP_MISSING, "Username").Error(), err.Error()) 47 | 48 | authenticator, err = NewBasicAuthenticator("username", "password") 49 | assert.NotNil(t, authenticator) 50 | assert.Nil(t, err) 51 | } 52 | 53 | func TestBasicAuthPassword(t *testing.T) { 54 | _, err := NewBasicAuthenticator("username", "{password}") 55 | assert.NotNil(t, err) 56 | assert.Equal(t, fmt.Errorf(ERRORMSG_PROP_INVALID, "Password").Error(), err.Error()) 57 | 58 | _, err = NewBasicAuthenticator("username", "\"password\"") 59 | assert.NotNil(t, err) 60 | assert.Equal(t, fmt.Errorf(ERRORMSG_PROP_INVALID, "Password").Error(), err.Error()) 61 | 62 | _, err = NewBasicAuthenticator("username", "") 63 | assert.NotNil(t, err) 64 | assert.Equal(t, fmt.Errorf(ERRORMSG_PROP_MISSING, "Password").Error(), err.Error()) 65 | 66 | authenticator, err := NewBasicAuthenticator("username", "password") 67 | assert.NotNil(t, authenticator) 68 | assert.Nil(t, err) 69 | } 70 | 71 | func TestBasicAuthAuthenticate(t *testing.T) { 72 | GetLogger().SetLogLevel(basicAuthTestLogLevel) 73 | authenticator := &BasicAuthenticator{ 74 | Username: "foo", 75 | Password: "bar", 76 | } 77 | 78 | assert.Equal(t, authenticator.AuthenticationType(), AUTHTYPE_BASIC) 79 | 80 | // Create a new Request object. 81 | builder, err := NewRequestBuilder("GET").ConstructHTTPURL("https://localhost/placeholder/url", nil, nil) 82 | assert.Nil(t, err) 83 | 84 | request, err := builder.Build() 85 | assert.Nil(t, err) 86 | assert.NotNil(t, request) 87 | 88 | // Test the "Authenticate" method to make sure the correct header is added to the Request. 89 | _ = authenticator.Authenticate(request) 90 | assert.Equal(t, request.Header.Get("Authorization"), "Basic Zm9vOmJhcg==") 91 | } 92 | 93 | func TestNewBasicAuthenticatorFromMap(t *testing.T) { 94 | _, err := newBasicAuthenticatorFromMap(nil) 95 | assert.NotNil(t, err) 96 | 97 | var props = map[string]string{ 98 | PROPNAME_USERNAME: "my-user", 99 | PROPNAME_PASSWORD: "", 100 | } 101 | _, err = newBasicAuthenticatorFromMap(props) 102 | assert.NotNil(t, err) 103 | 104 | props = map[string]string{ 105 | PROPNAME_USERNAME: "", 106 | PROPNAME_PASSWORD: "my-password", 107 | } 108 | _, err = newBasicAuthenticatorFromMap(props) 109 | assert.NotNil(t, err) 110 | 111 | props = map[string]string{ 112 | PROPNAME_USERNAME: "mookie", 113 | PROPNAME_PASSWORD: "betts", 114 | } 115 | authenticator, err := newBasicAuthenticatorFromMap(props) 116 | assert.Nil(t, err) 117 | assert.NotNil(t, authenticator) 118 | assert.Equal(t, "mookie", authenticator.Username) 119 | assert.Equal(t, "betts", authenticator.Password) 120 | } 121 | -------------------------------------------------------------------------------- /core/bearer_token_authenticator.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | // (C) Copyright IBM Corp. 2019. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | import ( 18 | "errors" 19 | "fmt" 20 | "net/http" 21 | ) 22 | 23 | // BearerTokenAuthenticator will take a user-supplied bearer token and adds 24 | // it to requests via an Authorization header of the form: 25 | // 26 | // Authorization: Bearer 27 | type BearerTokenAuthenticator struct { 28 | // The bearer token value to be used to authenticate request [required]. 29 | BearerToken string 30 | } 31 | 32 | // NewBearerTokenAuthenticator constructs a new BearerTokenAuthenticator instance. 33 | func NewBearerTokenAuthenticator(bearerToken string) (*BearerTokenAuthenticator, error) { 34 | obj := &BearerTokenAuthenticator{ 35 | BearerToken: bearerToken, 36 | } 37 | if err := obj.Validate(); err != nil { 38 | err = RepurposeSDKProblem(err, "validation-failed") 39 | return nil, err 40 | } 41 | return obj, nil 42 | } 43 | 44 | // newBearerTokenAuthenticator : Constructs a new BearerTokenAuthenticator instance from a map. 45 | func newBearerTokenAuthenticatorFromMap(properties map[string]string) (*BearerTokenAuthenticator, error) { 46 | if properties == nil { 47 | err := errors.New(ERRORMSG_PROPS_MAP_NIL) 48 | return nil, SDKErrorf(err, "", "missing-props", getComponentInfo()) 49 | } 50 | 51 | return NewBearerTokenAuthenticator(properties[PROPNAME_BEARER_TOKEN]) 52 | } 53 | 54 | // AuthenticationType returns the authentication type for this authenticator. 55 | func (BearerTokenAuthenticator) AuthenticationType() string { 56 | return AUTHTYPE_BEARER_TOKEN 57 | } 58 | 59 | // Authenticate adds bearer authentication information to the request. 60 | // 61 | // The bearer token will be added to the request's headers in the form: 62 | // 63 | // Authorization: Bearer 64 | func (authenticator *BearerTokenAuthenticator) Authenticate(request *http.Request) error { 65 | request.Header.Set("Authorization", fmt.Sprintf(`Bearer %s`, authenticator.BearerToken)) 66 | GetLogger().Debug("Authenticated outbound request (type=%s)\n", authenticator.AuthenticationType()) 67 | return nil 68 | } 69 | 70 | // Validate the authenticator's configuration. 71 | // 72 | // Ensures the bearer token is not Nil. 73 | func (authenticator BearerTokenAuthenticator) Validate() error { 74 | if authenticator.BearerToken == "" { 75 | err := fmt.Errorf(ERRORMSG_PROP_MISSING, "BearerToken") 76 | return SDKErrorf(err, "", "no-token", getComponentInfo()) 77 | } 78 | return nil 79 | } 80 | -------------------------------------------------------------------------------- /core/bearer_token_authenticator_test.go: -------------------------------------------------------------------------------- 1 | //go:build all || fast || auth 2 | 3 | package core 4 | 5 | // (C) Copyright IBM Corp. 2019. 6 | // 7 | // Licensed under the Apache License, Version 2.0 (the "License"); 8 | // you may not use this file except in compliance with the License. 9 | // You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, software 14 | // distributed under the License is distributed on an "AS IS" BASIS, 15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | // See the License for the specific language governing permissions and 17 | // limitations under the License. 18 | 19 | import ( 20 | "fmt" 21 | "testing" 22 | 23 | "github.com/stretchr/testify/assert" 24 | ) 25 | 26 | var ( 27 | // To enable debug logging during test execution, set this to "LevelDebug" 28 | bearerAuthTestLogLevel LogLevel = LevelError 29 | ) 30 | 31 | func TestBearerToken(t *testing.T) { 32 | authenticator := &BearerTokenAuthenticator{ 33 | BearerToken: "", 34 | } 35 | err := authenticator.Validate() 36 | assert.NotNil(t, err) 37 | assert.Equal(t, fmt.Errorf(ERRORMSG_PROP_MISSING, "BearerToken").Error(), err.Error()) 38 | 39 | authenticator, err = NewBearerTokenAuthenticator("my-bearer-token") 40 | assert.NotNil(t, authenticator) 41 | assert.Nil(t, err) 42 | } 43 | 44 | func TestBearerTokenAuthenticate(t *testing.T) { 45 | GetLogger().SetLogLevel(bearerAuthTestLogLevel) 46 | authenticator := &BearerTokenAuthenticator{ 47 | BearerToken: "my-bearer-token", 48 | } 49 | assert.Equal(t, authenticator.AuthenticationType(), AUTHTYPE_BEARER_TOKEN) 50 | 51 | // Create a new Request object. 52 | builder, err := NewRequestBuilder("GET").ConstructHTTPURL("https://localhost/placeholder/url", nil, nil) 53 | assert.Nil(t, err) 54 | 55 | request, err := builder.Build() 56 | assert.Nil(t, err) 57 | assert.NotNil(t, request) 58 | 59 | // Test the "Authenticate" method to make sure the correct header is added to the Request. 60 | _ = authenticator.Authenticate(request) 61 | assert.Equal(t, request.Header.Get("Authorization"), "Bearer my-bearer-token") 62 | } 63 | 64 | func TestNewBearerTokenAuthenticatorFromMap(t *testing.T) { 65 | _, err := newBearerTokenAuthenticatorFromMap(nil) 66 | assert.NotNil(t, err) 67 | 68 | var props = map[string]string{ 69 | PROPNAME_BEARER_TOKEN: "", 70 | } 71 | _, err = newBearerTokenAuthenticatorFromMap(props) 72 | assert.NotNil(t, err) 73 | 74 | props = map[string]string{ 75 | PROPNAME_BEARER_TOKEN: "my-token", 76 | } 77 | authenticator, err := newBearerTokenAuthenticatorFromMap(props) 78 | assert.Nil(t, err) 79 | assert.NotNil(t, authenticator) 80 | assert.Equal(t, "my-token", authenticator.BearerToken) 81 | } 82 | -------------------------------------------------------------------------------- /core/common_test.go: -------------------------------------------------------------------------------- 1 | //go:build all || fast || basesvc || retries || auth 2 | 3 | package core 4 | 5 | import ( 6 | "bytes" 7 | "encoding/json" 8 | "testing" 9 | ) 10 | 11 | // (C) Copyright IBM Corp. 2020, 2024. 12 | // 13 | // Licensed under the Apache License, Version 2.0 (the "License"); 14 | // you may not use this file except in compliance with the License. 15 | // You may obtain a copy of the License at 16 | // 17 | // http://www.apache.org/licenses/LICENSE-2.0 18 | // 19 | // Unless required by applicable law or agreed to in writing, software 20 | // distributed under the License is distributed on an "AS IS" BASIS, 21 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 22 | // See the License for the specific language governing permissions and 23 | // limitations under the License. 24 | 25 | // 26 | // This file contains definitions of various types that are shared among multiple testcase files. 27 | // 28 | 29 | type Foo struct { 30 | Name *string `json:"name,omitempty"` 31 | } 32 | 33 | func toJSON(obj interface{}) string { 34 | buf := new(bytes.Buffer) 35 | err := json.NewEncoder(buf).Encode(obj) 36 | if err != nil { 37 | panic(err) 38 | } 39 | return buf.String() 40 | } 41 | 42 | // Map containing environment variables used in testing. 43 | var testEnvironment = map[string]string{ 44 | "SERVICE_1_URL": "https://service1/api", 45 | "SERVICE_1_DISABLE_SSL": "true", 46 | "SERVICE_1_ENABLE_GZIP": "true", 47 | "SERVICE_1_AUTH_TYPE": "IaM", 48 | "SERVICE_1_APIKEY": "my-api-key", 49 | "SERVICE_1_CLIENT_ID": "my-client-id", 50 | "SERVICE_1_CLIENT_SECRET": "my-client-secret", 51 | "SERVICE_1_AUTH_URL": "https://iamhost/iam/api", 52 | "SERVICE_1_AUTH_DISABLE_SSL": "true", 53 | "SERVICE2_URL": "https://service2/api", 54 | "SERVICE2_DISABLE_SSL": "false", 55 | "SERVICE2_ENABLE_GZIP": "false", 56 | "SERVICE2_AUTH_TYPE": "bAsIC", 57 | "SERVICE2_USERNAME": "my-user", 58 | "SERVICE2_PASSWORD": "my-password", 59 | "SERVICE3_URL": "https://service3/api", 60 | "SERVICE3_DISABLE_SSL": "false", 61 | "SERVICE3_ENABLE_GZIP": "notabool", 62 | "SERVICE3_AUTH_TYPE": "Cp4D", 63 | "SERVICE3_AUTH_URL": "https://cp4dhost/cp4d/api", 64 | "SERVICE3_USERNAME": "my-cp4d-user", 65 | "SERVICE3_PASSWORD": "my-cp4d-password", 66 | "SERVICE3_AUTH_DISABLE_SSL": "false", 67 | "EQUAL_SERVICE_URL": "https://my=host.com/my=service/api", 68 | "EQUAL_SERVICE_APIKEY": "===my=iam=apikey===", 69 | "SERVICE6_AUTH_TYPE": "iam", 70 | "SERVICE6_APIKEY": "my-api-key", 71 | "SERVICE6_SCOPE": "A B C D", 72 | "SERVICE7_AUTH_TYPE": "container", 73 | "SERVICE7_CR_TOKEN_FILENAME": "crtoken.txt", 74 | "SERVICE7_IAM_PROFILE_NAME": "iam-user2", 75 | "SERVICE7_IAM_PROFILE_ID": "iam-id2", 76 | "SERVICE7_AUTH_URL": "https://iamhost/iam/api", 77 | "SERVICE7_CLIENT_ID": "iam-client2", 78 | "SERVICE7_CLIENT_SECRET": "iam-secret2", 79 | "SERVICE7_SCOPE": "scope2 scope3", 80 | "SERVICE8_AUTH_TYPE": "VPC", 81 | "SERVICE8_IAM_PROFILE_CRN": "crn:iam-profile1", 82 | "SERVICE8_AUTH_URL": "http://vpc.imds.com/api", 83 | "SERVICE9_AUTH_TYPE": "bearerToken", 84 | "SERVICE9_BEARER_TOKEN": "my-token", 85 | "SERVICE10_AUTH_TYPE": "noauth", 86 | "SERVICE11_AUTH_TYPE": "bad_auth_type", 87 | "SERVICE12_APIKEY": "my-apikey", 88 | "SERVICE13_IAM_PROFILE_NAME": "iam-user2", 89 | "SERVICE14_AUTH_TYPE": "mcsp", 90 | "SERVICE14_AUTH_URL": "https://mcsp.ibm.com", 91 | "SERVICE14_APIKEY": "my-api-key", 92 | "SERVICE14_AUTH_DISABLE_SSL": "true", 93 | "SERVICE15_AUTH_URL": "https://iam.assume.ibm.com", 94 | "SERVICE15_AUTH_TYPE": "IAMAssUME", 95 | "SERVICE15_APIKEY": "my-apikey", 96 | "SERVICE15_IAM_PROFILE_NAME": "profile-1", 97 | "SERVICE15_IAM_ACCOUNT_ID": "account-1", 98 | "SERVICE16_AUTH_TYPE": "mcspv2", 99 | "SERVICE16_APIKEY": "my-api-key", 100 | "SERVICE16_AUTH_URL": "https://mcspv2.ibm.com", 101 | "SERVICE16_SCOPE_COLLECTION_TYPE": "accounts", 102 | "SERVICE16_SCOPE_ID": "global_accounts", 103 | "SERVICE16_INCLUDE_BUILTIN_ACTIONS": "true", 104 | "SERVICE16_INCLUDE_CUSTOM_ACTIONS": "true", 105 | "SERVICE16_INCLUDE_ROLES": "false", 106 | "SERVICE16_PREFIX_ROLES": "true", 107 | "SERVICE16_CALLER_EXT_CLAIM": `{"productID":"prod456"}`, 108 | "SERVICE16_AUTH_DISABLE_SSL": "true", 109 | } 110 | 111 | // setTestEnvironment sets the environment variables described in our map. 112 | // The environment variables are restored to its original value after the test. 113 | func setTestEnvironment(t *testing.T) { 114 | for key, value := range testEnvironment { 115 | t.Setenv(key, value) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /core/config_utils.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | // (C) Copyright IBM Corp. 2019. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | import ( 18 | "bufio" 19 | "encoding/json" 20 | "fmt" 21 | "os" 22 | "path" 23 | "strings" 24 | ) 25 | 26 | const ( 27 | // IBM_CREDENTIAL_FILE_ENVVAR is the environment key used to find the path to 28 | // a credentials file. 29 | IBM_CREDENTIAL_FILE_ENVVAR = "IBM_CREDENTIALS_FILE" 30 | 31 | // DEFAULT_CREDENTIAL_FILE_NAME is the default filename for a credentials file. 32 | // It is used when "IBM_CREDENTIALS_FILE" is not specified. The filename will 33 | // be searched for within the program's working directory, and then the OS's 34 | // current user directory. 35 | DEFAULT_CREDENTIAL_FILE_NAME = "ibm-credentials.env" 36 | ) 37 | 38 | // GetServiceProperties returns a map containing configuration properties for the specified service 39 | // that are retrieved from external configuration sources in the following precedence order: 40 | // 1) credential file 41 | // 2) environment variables 42 | // 3) VCAP_SERVICES 43 | // 44 | // 'serviceName' is used as a filter against the property names. For example, if serviceName is 45 | // passed in as "my_service", then configuration properties whose names begin with "MY_SERVICE_" 46 | // will be returned in the map. 47 | func GetServiceProperties(serviceName string) (serviceProps map[string]string, err error) { 48 | serviceProps, err = getServiceProperties(serviceName) 49 | err = RepurposeSDKProblem(err, "get-props-error") 50 | return 51 | } 52 | 53 | // getServiceProperties: This function will retrieve configuration properties for the specified service 54 | // from external config sources in the following precedence order: 55 | // 1) credential file 56 | // 2) environment variables 57 | // 3) VCAP_SERVICES 58 | func getServiceProperties(serviceName string) (serviceProps map[string]string, err error) { 59 | 60 | if serviceName == "" { 61 | err = fmt.Errorf("serviceName was not specified") 62 | err = SDKErrorf(err, "", "no-service-name", getComponentInfo()) 63 | return 64 | } 65 | 66 | GetLogger().Debug("Retrieving config properties for service '%s'\n", serviceName) 67 | 68 | // First try to retrieve service properties from a credential file. 69 | serviceProps = getServicePropertiesFromCredentialFile(serviceName) 70 | 71 | // Next, try to retrieve them from environment variables. 72 | if serviceProps == nil { 73 | serviceProps = getServicePropertiesFromEnvironment(serviceName) 74 | } 75 | 76 | // Finally, try to retrieve them from VCAP_SERVICES. 77 | if serviceProps == nil { 78 | serviceProps = getServicePropertiesFromVCAP(serviceName) 79 | } 80 | 81 | GetLogger().Debug("Retrieved %d properties\n", len(serviceProps)) 82 | 83 | return 84 | } 85 | 86 | // getServicePropertiesFromCredentialFile: returns a map containing properties found within a credential file 87 | // that are associated with the specified credentialKey. Returns a nil map if no properties are found. 88 | // Credential file search order: 89 | // 1) ${IBM_CREDENTIALS_FILE} 90 | // 2) /ibm-credentials.env 91 | // 3) /ibm-credentials.env 92 | func getServicePropertiesFromCredentialFile(credentialKey string) map[string]string { 93 | 94 | // Check the search order for the credential file that we'll attempt to load: 95 | var credentialFilePath string 96 | 97 | // 1) ${IBM_CREDENTIALS_FILE} 98 | envPath := os.Getenv(IBM_CREDENTIAL_FILE_ENVVAR) 99 | if _, err := os.Stat(envPath); err == nil { 100 | credentialFilePath = envPath 101 | } 102 | 103 | // 2) /ibm-credentials.env 104 | if credentialFilePath == "" { 105 | dir, _ := os.Getwd() 106 | var filePath = path.Join(dir, DEFAULT_CREDENTIAL_FILE_NAME) 107 | if _, err := os.Stat(filePath); err == nil { 108 | credentialFilePath = filePath 109 | } 110 | } 111 | 112 | // 3) /ibm-credentials.env 113 | if credentialFilePath == "" { 114 | var filePath = path.Join(UserHomeDir(), DEFAULT_CREDENTIAL_FILE_NAME) 115 | if _, err := os.Stat(filePath); err == nil { 116 | credentialFilePath = filePath 117 | } 118 | } 119 | 120 | // If we found a file to load, then load it. 121 | if credentialFilePath != "" { 122 | file, err := os.Open(credentialFilePath) // #nosec G304 123 | if err != nil { 124 | return nil 125 | } 126 | defer file.Close() // #nosec G307 127 | 128 | // Collect the contents of the credential file in a string array. 129 | lines := make([]string, 0) 130 | scanner := bufio.NewScanner(file) 131 | for scanner.Scan() { 132 | lines = append(lines, scanner.Text()) 133 | } 134 | 135 | // Parse the file contents into name/value pairs. 136 | return parsePropertyStrings(credentialKey, lines) 137 | } 138 | 139 | return nil 140 | } 141 | 142 | // getServicePropertiesFromEnvironment: returns a map containing properties found within the environment 143 | // that are associated with the specified credentialKey. Returns a nil map if no properties are found. 144 | func getServicePropertiesFromEnvironment(credentialKey string) map[string]string { 145 | return parsePropertyStrings(credentialKey, os.Environ()) 146 | } 147 | 148 | // getServicePropertiesFromVCAP: returns a map containing properties found within the VCAP_SERVICES 149 | // environment variable for the specified credentialKey (service name). Returns a nil map if no properties are found. 150 | func getServicePropertiesFromVCAP(credentialKey string) map[string]string { 151 | credentials := loadFromVCAPServices(credentialKey) 152 | if credentials != nil { 153 | props := make(map[string]string) 154 | if credentials.URL != "" { 155 | props[PROPNAME_SVC_URL] = credentials.URL 156 | } 157 | 158 | if credentials.Username != "" { 159 | props[PROPNAME_USERNAME] = credentials.Username 160 | } 161 | 162 | if credentials.Password != "" { 163 | props[PROPNAME_PASSWORD] = credentials.Password 164 | } 165 | 166 | if credentials.APIKey != "" { 167 | props[PROPNAME_APIKEY] = credentials.APIKey 168 | } 169 | 170 | // If no values were actually found in this credential entry, then bail out now. 171 | if len(props) == 0 { 172 | return nil 173 | } 174 | 175 | // Make a (hopefully good) guess at the auth type. 176 | authType := "" 177 | if props[PROPNAME_APIKEY] != "" { 178 | authType = AUTHTYPE_IAM 179 | } else if props[PROPNAME_USERNAME] != "" || props[PROPNAME_PASSWORD] != "" { 180 | authType = AUTHTYPE_BASIC 181 | } else { 182 | authType = AUTHTYPE_IAM 183 | } 184 | props[PROPNAME_AUTH_TYPE] = authType 185 | 186 | return props 187 | } 188 | 189 | return nil 190 | } 191 | 192 | // parsePropertyStrings: accepts an array of strings of the form "=" and parses/filters them to 193 | // produce a map of properties associated with the specified credentialKey. 194 | func parsePropertyStrings(credentialKey string, propertyStrings []string) map[string]string { 195 | if len(propertyStrings) == 0 { 196 | return nil 197 | } 198 | 199 | props := make(map[string]string) 200 | credentialKey = strings.ToUpper(credentialKey) 201 | credentialKey = strings.Replace(credentialKey, "-", "_", -1) 202 | credentialKey += "_" 203 | for _, propertyString := range propertyStrings { 204 | 205 | // Trim the property string and ignore any blank or comment lines. 206 | propertyString = strings.TrimSpace(propertyString) 207 | if propertyString == "" || strings.HasPrefix(propertyString, "#") { 208 | continue 209 | } 210 | 211 | // Parse the property string into name and value tokens 212 | var tokens = strings.SplitN(propertyString, "=", 2) 213 | if len(tokens) == 2 { 214 | // Does the name start with the credential key? 215 | // If so, then extract the property name by filtering out the credential key, 216 | // then store the name/value pair in the map. 217 | if strings.HasPrefix(tokens[0], credentialKey) && (len(tokens[0]) > len(credentialKey)) { 218 | name := tokens[0][len(credentialKey):] 219 | value := strings.TrimSpace(tokens[1]) 220 | props[name] = value 221 | } 222 | } 223 | } 224 | 225 | if len(props) == 0 { 226 | return nil 227 | } 228 | return props 229 | } 230 | 231 | // Service : The service 232 | type service struct { 233 | Name string `json:"name,omitempty"` 234 | Credentials *credential `json:"credentials,omitempty"` 235 | } 236 | 237 | // Credential : The service credential 238 | type credential struct { 239 | URL string `json:"url,omitempty"` 240 | Username string `json:"username,omitempty"` 241 | Password string `json:"password,omitempty"` 242 | APIKey string `json:"apikey,omitempty"` 243 | } 244 | 245 | // LoadFromVCAPServices : returns the credential of the service 246 | func loadFromVCAPServices(serviceName string) *credential { 247 | vcapServices := os.Getenv("VCAP_SERVICES") 248 | if vcapServices != "" { 249 | var rawServices map[string][]service 250 | if err := json.Unmarshal([]byte(vcapServices), &rawServices); err != nil { 251 | return nil 252 | } 253 | for _, serviceEntries := range rawServices { 254 | for _, service := range serviceEntries { 255 | if service.Name == serviceName { 256 | return service.Credentials 257 | } 258 | } 259 | } 260 | if serviceList, exists := rawServices[serviceName]; exists && len(serviceList) > 0 { 261 | return serviceList[0].Credentials 262 | } 263 | } 264 | return nil 265 | } 266 | -------------------------------------------------------------------------------- /core/config_utils_test.go: -------------------------------------------------------------------------------- 1 | //go:build all || fast || basesvc || auth 2 | 3 | package core 4 | 5 | // (C) Copyright IBM Corp. 2019, 2024. 6 | // 7 | // Licensed under the Apache License, Version 2.0 (the "License"); 8 | // you may not use this file except in compliance with the License. 9 | // You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, software 14 | // distributed under the License is distributed on an "AS IS" BASIS, 15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | // See the License for the specific language governing permissions and 17 | // limitations under the License. 18 | 19 | import ( 20 | "os" 21 | "path" 22 | "strings" 23 | "testing" 24 | 25 | "github.com/stretchr/testify/assert" 26 | ) 27 | 28 | var ( 29 | // To enable debug logging during test execution, set this to "LevelDebug" 30 | configUtilsTestLogLevel LogLevel = LevelError 31 | ) 32 | 33 | const vcapServicesKey = "VCAP_SERVICES" 34 | 35 | // Sets a test VCAP_SERVICES value in the environment for testing. 36 | func setTestVCAP(t *testing.T) { 37 | data, err := os.ReadFile("../resources/vcap_services.json") 38 | if assert.Nil(t, err) { 39 | t.Setenv(vcapServicesKey, string(data)) 40 | } 41 | } 42 | 43 | func TestGetServicePropertiesError(t *testing.T) { 44 | GetLogger().SetLogLevel(configUtilsTestLogLevel) 45 | _, err := getServiceProperties("") 46 | assert.NotNil(t, err) 47 | } 48 | 49 | func TestGetServicePropertiesNoConfig(t *testing.T) { 50 | GetLogger().SetLogLevel(configUtilsTestLogLevel) 51 | props, err := getServiceProperties("not_a_service") 52 | assert.Nil(t, err) 53 | assert.Nil(t, props) 54 | } 55 | 56 | func TestGetServicePropertiesFromCredentialFile(t *testing.T) { 57 | GetLogger().SetLogLevel(configUtilsTestLogLevel) 58 | pwd, _ := os.Getwd() 59 | credentialFilePath := path.Join(pwd, "/../resources/my-credentials.env") 60 | t.Setenv("IBM_CREDENTIALS_FILE", credentialFilePath) 61 | 62 | props, err := GetServiceProperties("service-1") 63 | assert.Nil(t, err) 64 | assert.NotNil(t, props) 65 | assert.Equal(t, "https://service1/api", props[PROPNAME_SVC_URL]) 66 | assert.Equal(t, "true", props[PROPNAME_SVC_DISABLE_SSL]) 67 | assert.Equal(t, "true", props[PROPNAME_SVC_ENABLE_GZIP]) 68 | assert.Equal(t, strings.ToUpper(AUTHTYPE_IAM), strings.ToUpper(props[PROPNAME_AUTH_TYPE])) 69 | assert.Equal(t, "my-api-key", props[PROPNAME_APIKEY]) 70 | assert.Equal(t, "my-client-id", props[PROPNAME_CLIENT_ID]) 71 | assert.Equal(t, "my-client-secret", props[PROPNAME_CLIENT_SECRET]) 72 | assert.Equal(t, "https://iamhost/iam/api", props[PROPNAME_AUTH_URL]) 73 | assert.Equal(t, "true", props[PROPNAME_AUTH_DISABLE_SSL]) 74 | 75 | props, err = getServiceProperties("service2") 76 | assert.Nil(t, err) 77 | assert.NotNil(t, props) 78 | assert.Equal(t, "https://service2/api", props[PROPNAME_SVC_URL]) 79 | assert.Equal(t, "false", props[PROPNAME_SVC_DISABLE_SSL]) 80 | assert.Equal(t, "false", props[PROPNAME_SVC_ENABLE_GZIP]) 81 | assert.Equal(t, strings.ToUpper(AUTHTYPE_BASIC), strings.ToUpper(props["AUTHTYPE"])) 82 | assert.Equal(t, "my-user", props[PROPNAME_USERNAME]) 83 | assert.Equal(t, "my-password", props[PROPNAME_PASSWORD]) 84 | 85 | props, err = getServiceProperties("service3") 86 | assert.Nil(t, err) 87 | assert.NotNil(t, props) 88 | assert.Equal(t, "https://service3/api", props[PROPNAME_SVC_URL]) 89 | assert.Equal(t, "false", props[PROPNAME_SVC_DISABLE_SSL]) 90 | assert.Equal(t, "notabool", props[PROPNAME_SVC_ENABLE_GZIP]) 91 | assert.Equal(t, strings.ToUpper(AUTHTYPE_CP4D), strings.ToUpper(props["AUTHTYPE"])) 92 | assert.Equal(t, "my-cp4d-user", props[PROPNAME_USERNAME]) 93 | assert.Equal(t, "my-cp4d-password", props[PROPNAME_PASSWORD]) 94 | assert.Equal(t, "https://cp4dhost/cp4d/api", props[PROPNAME_AUTH_URL]) 95 | assert.Equal(t, "false", props[PROPNAME_AUTH_DISABLE_SSL]) 96 | 97 | props, err = GetServiceProperties("equal_service") 98 | assert.Nil(t, err) 99 | assert.NotNil(t, props) 100 | assert.Equal(t, "=https:/my=host.com/my=service/api", props[PROPNAME_SVC_URL]) 101 | assert.Equal(t, "=my=api=key=", props[PROPNAME_APIKEY]) 102 | 103 | props, err = getServiceProperties("not_a_service") 104 | assert.Nil(t, err) 105 | assert.Nil(t, props) 106 | 107 | props, err = getServiceProperties("service6") 108 | assert.Nil(t, err) 109 | assert.NotNil(t, props) 110 | assert.Equal(t, "IAM", props[PROPNAME_AUTH_TYPE]) 111 | assert.Equal(t, "my-api-key", props[PROPNAME_APIKEY]) 112 | assert.Equal(t, "https://iamhost/iam/api", props[PROPNAME_AUTH_URL]) 113 | assert.Equal(t, "scope1 scope2 scope3", props[PROPNAME_SCOPE]) 114 | } 115 | 116 | func TestGetServicePropertiesFromEnvironment(t *testing.T) { 117 | GetLogger().SetLogLevel(configUtilsTestLogLevel) 118 | setTestEnvironment(t) 119 | 120 | props, err := GetServiceProperties("service-1") 121 | assert.Nil(t, err) 122 | assert.NotNil(t, props) 123 | assert.Equal(t, "https://service1/api", props[PROPNAME_SVC_URL]) 124 | assert.Equal(t, "true", props[PROPNAME_SVC_DISABLE_SSL]) 125 | assert.Equal(t, "true", props[PROPNAME_SVC_ENABLE_GZIP]) 126 | assert.Equal(t, strings.ToUpper(AUTHTYPE_IAM), strings.ToUpper(props[PROPNAME_AUTH_TYPE])) 127 | assert.Equal(t, "my-api-key", props[PROPNAME_APIKEY]) 128 | assert.Equal(t, "my-client-id", props[PROPNAME_CLIENT_ID]) 129 | assert.Equal(t, "my-client-secret", props[PROPNAME_CLIENT_SECRET]) 130 | assert.Equal(t, "https://iamhost/iam/api", props[PROPNAME_AUTH_URL]) 131 | assert.Equal(t, "true", props[PROPNAME_AUTH_DISABLE_SSL]) 132 | 133 | props, err = getServiceProperties("service2") 134 | assert.Nil(t, err) 135 | assert.NotNil(t, props) 136 | assert.Equal(t, "https://service2/api", props[PROPNAME_SVC_URL]) 137 | assert.Equal(t, "false", props[PROPNAME_SVC_DISABLE_SSL]) 138 | assert.Equal(t, "false", props[PROPNAME_SVC_ENABLE_GZIP]) 139 | assert.Equal(t, strings.ToUpper(AUTHTYPE_BASIC), strings.ToUpper(props[PROPNAME_AUTH_TYPE])) 140 | assert.Equal(t, "my-user", props[PROPNAME_USERNAME]) 141 | assert.Equal(t, "my-password", props[PROPNAME_PASSWORD]) 142 | 143 | props, err = getServiceProperties("service3") 144 | assert.Nil(t, err) 145 | assert.NotNil(t, props) 146 | assert.Equal(t, "https://service3/api", props[PROPNAME_SVC_URL]) 147 | assert.Equal(t, "false", props[PROPNAME_SVC_DISABLE_SSL]) 148 | assert.Equal(t, "notabool", props[PROPNAME_SVC_ENABLE_GZIP]) 149 | assert.Equal(t, strings.ToUpper(AUTHTYPE_CP4D), strings.ToUpper(props[PROPNAME_AUTH_TYPE])) 150 | assert.Equal(t, "my-cp4d-user", props[PROPNAME_USERNAME]) 151 | assert.Equal(t, "my-cp4d-password", props[PROPNAME_PASSWORD]) 152 | assert.Equal(t, "https://cp4dhost/cp4d/api", props[PROPNAME_AUTH_URL]) 153 | assert.Equal(t, "false", props[PROPNAME_AUTH_DISABLE_SSL]) 154 | 155 | props, err = GetServiceProperties("equal_service") 156 | assert.Nil(t, err) 157 | assert.NotNil(t, props) 158 | assert.Equal(t, "https://my=host.com/my=service/api", props[PROPNAME_SVC_URL]) 159 | assert.Equal(t, "===my=iam=apikey===", props[PROPNAME_APIKEY]) 160 | 161 | props, err = getServiceProperties("not_a_service") 162 | assert.Nil(t, err) 163 | assert.Nil(t, props) 164 | 165 | props, err = getServiceProperties("service6") 166 | assert.Nil(t, err) 167 | assert.NotNil(t, props) 168 | assert.Equal(t, "iam", props[PROPNAME_AUTH_TYPE]) 169 | assert.Equal(t, "my-api-key", props[PROPNAME_APIKEY]) 170 | assert.Equal(t, "A B C D", props[PROPNAME_SCOPE]) 171 | } 172 | 173 | func TestGetServicePropertiesFromVCAP(t *testing.T) { 174 | GetLogger().SetLogLevel(configUtilsTestLogLevel) 175 | setTestVCAP(t) 176 | 177 | props, err := getServiceProperties("service-1") 178 | assert.Nil(t, err) 179 | assert.NotNil(t, props) 180 | assert.Equal(t, "https://service1/api", props[PROPNAME_SVC_URL]) 181 | assert.Equal(t, "my-vcap-apikey1", props[PROPNAME_APIKEY]) 182 | assert.Equal(t, "my-vcap-user", props[PROPNAME_USERNAME]) 183 | assert.Equal(t, "my-vcap-password", props[PROPNAME_PASSWORD]) 184 | assert.Equal(t, strings.ToUpper(AUTHTYPE_IAM), strings.ToUpper(props[PROPNAME_AUTH_TYPE])) 185 | 186 | props, err = getServiceProperties("service2") 187 | assert.Nil(t, err) 188 | assert.NotNil(t, props) 189 | assert.Equal(t, "https://service2/api", props[PROPNAME_SVC_URL]) 190 | assert.Equal(t, "", props[PROPNAME_APIKEY]) 191 | assert.Equal(t, "my-vcap-user", props[PROPNAME_USERNAME]) 192 | assert.Equal(t, "my-vcap-password", props[PROPNAME_PASSWORD]) 193 | assert.Equal(t, strings.ToUpper(AUTHTYPE_BASIC), strings.ToUpper(props[PROPNAME_AUTH_TYPE])) 194 | 195 | props, err = getServiceProperties("service3") 196 | assert.Nil(t, err) 197 | assert.NotNil(t, props) 198 | assert.Equal(t, "https://service3/api", props[PROPNAME_SVC_URL]) 199 | assert.Equal(t, "my-vcap-apikey3", props[PROPNAME_APIKEY]) 200 | assert.Equal(t, "", props[PROPNAME_USERNAME]) 201 | assert.Equal(t, "", props[PROPNAME_PASSWORD]) 202 | assert.Equal(t, strings.ToUpper(AUTHTYPE_IAM), strings.ToUpper(props[PROPNAME_AUTH_TYPE])) 203 | } 204 | 205 | func TestLoadFromVCAPServicesWithServiceEntries(t *testing.T) { 206 | GetLogger().SetLogLevel(configUtilsTestLogLevel) 207 | setTestVCAP(t) 208 | // Verify we checked service entry names first 209 | credential1 := loadFromVCAPServices("service_entry_key_and_key_to_service_entries") 210 | isNotNil := assert.NotNil(t, credential1, "Credentials1 should not be nil") 211 | if !isNotNil { 212 | return 213 | } 214 | assert.Equal(t, "not-a-username", credential1.Username) 215 | assert.Equal(t, "not-a-password", credential1.Password) 216 | assert.Equal(t, "https://on.the.toolchainplatform.net/devops-insights/api", credential1.URL) 217 | // Verify we checked keys that map to lists of service entries 218 | credential2 := loadFromVCAPServices("key_to_service_entry_1") 219 | isNotNil = assert.NotNil(t, credential2, "Credentials2 should not be nil") 220 | if !isNotNil { 221 | return 222 | } 223 | assert.Equal(t, "my-vcap-apikey3", credential2.APIKey) 224 | assert.Equal(t, "https://service3/api", credential2.URL) 225 | credential3 := loadFromVCAPServices("key_to_service_entry_2") 226 | isNotNil = assert.NotNil(t, credential3, "Credentials3 should not be nil") 227 | if !isNotNil { 228 | return 229 | } 230 | assert.Equal(t, "not-a-username-3", credential3.Username) 231 | assert.Equal(t, "not-a-password-3", credential3.Password) 232 | assert.Equal(t, "https://on.the.toolchainplatform.net/devops-insights-3/api", credential3.URL) 233 | } 234 | 235 | func TestLoadFromVCAPServicesEmptyService(t *testing.T) { 236 | GetLogger().SetLogLevel(configUtilsTestLogLevel) 237 | setTestVCAP(t) 238 | // Verify we checked service entry names first 239 | credential := loadFromVCAPServices("empty_service") 240 | assert.Nil(t, credential, "Credentials should not be nil") 241 | } 242 | 243 | func TestLoadFromVCAPServicesNoCredentials(t *testing.T) { 244 | GetLogger().SetLogLevel(configUtilsTestLogLevel) 245 | setTestVCAP(t) 246 | // Verify we checked service entry names first 247 | credential := loadFromVCAPServices("no-creds-service") 248 | assert.Nil(t, credential) 249 | } 250 | 251 | func TestLoadFromVCAPServicesWithEmptyString(t *testing.T) { 252 | GetLogger().SetLogLevel(configUtilsTestLogLevel) 253 | credential := loadFromVCAPServices("watson") 254 | assert.Nil(t, credential, "Credentials should nil") 255 | } 256 | 257 | func TestLoadFromVCAPServicesWithInvalidJSON(t *testing.T) { 258 | GetLogger().SetLogLevel(configUtilsTestLogLevel) 259 | vcapServicesFail := `{ 260 | "watson": [ 261 | "credentials": { 262 | "url": "https://api.us-south.compare-comply.watson.cloud.ibm.com", 263 | "username": "bogus username", 264 | "password": "bogus password", 265 | "apikey": "bogus apikey" 266 | } 267 | }] 268 | }` 269 | t.Setenv("VCAP_SERVICES", vcapServicesFail) 270 | credential := loadFromVCAPServices("watson") 271 | assert.Nil(t, credential, "Credentials should be nil") 272 | } 273 | -------------------------------------------------------------------------------- /core/constants.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | // (C) Copyright IBM Corp. 2019, 2025. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | const ( 18 | // Supported authentication types. 19 | AUTHTYPE_BASIC = "basic" 20 | AUTHTYPE_BEARER_TOKEN = "bearerToken" 21 | AUTHTYPE_NOAUTH = "noAuth" 22 | AUTHTYPE_IAM = "iam" 23 | AUTHTYPE_IAM_ASSUME = "iamAssume" 24 | AUTHTYPE_CP4D = "cp4d" 25 | AUTHTYPE_CONTAINER = "container" 26 | AUTHTYPE_VPC = "vpc" 27 | AUTHTYPE_MCSP = "mcsp" 28 | AUTHTYPE_MCSPV2 = "mcspv2" 29 | 30 | // Names of properties that can be defined as part of an external configuration (credential file, env vars, etc.). 31 | // Example: export MYSERVICE_URL=https://myurl 32 | 33 | // Service client properties. 34 | PROPNAME_SVC_URL = "URL" 35 | PROPNAME_SVC_DISABLE_SSL = "DISABLE_SSL" 36 | PROPNAME_SVC_ENABLE_GZIP = "ENABLE_GZIP" 37 | PROPNAME_SVC_ENABLE_RETRIES = "ENABLE_RETRIES" 38 | PROPNAME_SVC_MAX_RETRIES = "MAX_RETRIES" 39 | PROPNAME_SVC_RETRY_INTERVAL = "RETRY_INTERVAL" 40 | 41 | // Authenticator properties. 42 | PROPNAME_AUTH_TYPE = "AUTH_TYPE" 43 | PROPNAME_USERNAME = "USERNAME" 44 | PROPNAME_PASSWORD = "PASSWORD" 45 | PROPNAME_BEARER_TOKEN = "BEARER_TOKEN" 46 | PROPNAME_AUTH_URL = "AUTH_URL" 47 | PROPNAME_AUTH_DISABLE_SSL = "AUTH_DISABLE_SSL" 48 | PROPNAME_APIKEY = "APIKEY" 49 | PROPNAME_REFRESH_TOKEN = "REFRESH_TOKEN" // #nosec G101 50 | PROPNAME_CLIENT_ID = "CLIENT_ID" 51 | PROPNAME_CLIENT_SECRET = "CLIENT_SECRET" 52 | PROPNAME_SCOPE = "SCOPE" 53 | PROPNAME_CRTOKEN_FILENAME = "CR_TOKEN_FILENAME" // #nosec G101 54 | PROPNAME_IAM_PROFILE_CRN = "IAM_PROFILE_CRN" 55 | PROPNAME_IAM_PROFILE_NAME = "IAM_PROFILE_NAME" 56 | PROPNAME_IAM_PROFILE_ID = "IAM_PROFILE_ID" 57 | PROPNAME_IAM_ACCOUNT_ID = "IAM_ACCOUNT_ID" 58 | PROPNAME_SCOPE_COLLECTION_TYPE = "SCOPE_COLLECTION_TYPE" 59 | PROPNAME_SCOPE_ID = "SCOPE_ID" 60 | PROPNAME_INCLUDE_BUILTIN_ACTIONS = "INCLUDE_BUILTIN_ACTIONS" 61 | PROPNAME_INCLUDE_CUSTOM_ACTIONS = "INCLUDE_CUSTOM_ACTIONS" 62 | PROPNAME_INCLUDE_ROLES = "INCLUDE_ROLES" 63 | PROPNAME_PREFIX_ROLES = "PREFIX_ROLES" 64 | PROPNAME_CALLER_EXT_CLAIM = "CALLER_EXT_CLAIM" 65 | 66 | // SSL error 67 | SSL_CERTIFICATION_ERROR = "x509: certificate" 68 | 69 | // Common error messages. 70 | ERRORMSG_PROP_MISSING = "The %s property is required but was not specified." 71 | ERRORMSG_PROP_INVALID = "The %s property is invalid. Please remove any surrounding {, }, or \" characters." 72 | ERRORMSG_EXCLUSIVE_PROPS_ERROR = "Exactly one of %s or %s must be specified." 73 | ERRORMSG_ATLEAST_ONE_PROP_ERROR = "At least one of %s or %s must be specified." 74 | ERRORMSG_ATMOST_ONE_PROP_ERROR = "At most one of %s or %s may be specified." 75 | ERRORMSG_NO_AUTHENTICATOR = "Authentication information was not properly configured." 76 | ERRORMSG_AUTHTYPE_UNKNOWN = "Unrecognized authentication type: %s" 77 | ERRORMSG_PROPS_MAP_NIL = "The 'properties' map cannot be nil." 78 | ERRORMSG_SSL_VERIFICATION_FAILED = "The connection failed because the SSL certificate is not valid. To use a " + 79 | "self-signed certificate, disable verification of the server's SSL certificate " + 80 | "by invoking the DisableSSLVerification() function on your service instance " + 81 | "and/or use the DisableSSLVerification option of the authenticator." 82 | ERRORMSG_AUTHENTICATE_ERROR = "An error occurred while performing the 'authenticate' step: %s" 83 | ERRORMSG_READ_RESPONSE_BODY = "An error occurred while reading the response body: %s" 84 | ERRORMSG_UNEXPECTED_RESPONSE = "The response contained unexpected content, Content-Type=%s, operation resultType=%s" 85 | ERRORMSG_UNMARSHAL_RESPONSE_BODY = "An error occurred while processing the HTTP response: %s" 86 | ERRORMSG_NIL_SLICE = "The 'slice' parameter cannot be nil" 87 | ERRORMSG_PARAM_NOT_SLICE = "The 'slice' parameter must be a slice" 88 | ERRORMSG_MARSHAL_SLICE = "An error occurred while marshalling the slice: %s" 89 | ERRORMSG_CONVERT_SLICE = "An error occurred while converting 'slice' to string slice" 90 | ERRORMSG_UNEXPECTED_STATUS_CODE = "Unexpected HTTP status code %d (%s)" 91 | ERRORMSG_UNMARSHAL_AUTH_RESPONSE = "error unmarshalling authentication response: %s" 92 | ERRORMSG_UNABLE_RETRIEVE_CRTOKEN = "unable to retrieve compute resource token value: %s" // #nosec G101 93 | ERRORMSG_IAM_GETTOKEN_ERROR = "IAM 'get token' error, status code %d received from '%s': %s" // #nosec G101 94 | ERRORMSG_UNABLE_RETRIEVE_IITOKEN = "unable to retrieve instance identity token value: %s" // #nosec G101 95 | ERRORMSG_VPCMDS_OPERATION_ERROR = "VPC metadata service error, status code %d received from '%s': %s" 96 | ERRORMSG_ACCOUNTID_PROP_ERROR = "IAMAccountID must be specified if and only if IAMProfileName is specified" 97 | ERRORMSG_PROP_PARSE_ERROR = "error parsing configuration property %s, value=%s" 98 | 99 | // The name of this module - matches the value in the go.mod file. 100 | MODULE_NAME = "github.com/IBM/go-sdk-core/v5" 101 | ) 102 | -------------------------------------------------------------------------------- /core/core_suite_test.go: -------------------------------------------------------------------------------- 1 | package core_test 2 | 3 | // (C) Copyright IBM Corp. 2020. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | import ( 18 | "testing" 19 | 20 | . "github.com/onsi/ginkgo" 21 | . "github.com/onsi/gomega" 22 | ) 23 | 24 | func TestCore(t *testing.T) { 25 | RegisterFailHandler(Fail) 26 | RunSpecs(t, "Core Suite") 27 | } 28 | -------------------------------------------------------------------------------- /core/datetime.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | /** 4 | * (C) Copyright IBM Corp. 2020. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | import ( 20 | "time" 21 | 22 | "github.com/go-openapi/strfmt" 23 | ) 24 | 25 | // Customize the strfmt DateTime parsing and formatting for our use. 26 | func init() { 27 | // Force date-time serialization to use the UTC representation. 28 | strfmt.NormalizeTimeForMarshal = NormalizeDateTimeUTC 29 | 30 | // These formatting layouts (supported by time.Time.Format()) are added to the set of layouts used 31 | // by the strfmt.DateTime unmarshalling function(s). 32 | 33 | // RFC 3339 but with 2-digit tz-offset. 34 | // yyyy-MM-ddThh:mm:ss.SSS, where tz-offset is 'Z', +HH or -HH 35 | rfc3339TZ2Layout := "2006-01-02T15:04:05.000Z07" 36 | 37 | // RFC 3339 but with only seconds precision. 38 | // yyyy-MM-ddThh:mm:ss, where tz-offset is 'Z', +HH:MM or -HH:MM 39 | secsPrecisionLayout := "2006-01-02T15:04:05Z07:00" 40 | // Seconds precision with no colon in tz-offset 41 | secsPrecisionNoColonLayout := "2006-01-02T15:04:05Z0700" 42 | // Seconds precision with 2-digit tz-offset 43 | secsPrecisionTZ2Layout := "2006-01-02T15:04:05Z07" 44 | 45 | // RFC 3339 but with only minutes precision. 46 | // yyyy-MM-ddThh:mm, where tz-offset is 'Z' or +HH:MM or -HH:MM 47 | minPrecisionLayout := "2006-01-02T15:04Z07:00" 48 | // Minutes precision with no colon in tz-offset 49 | minPrecisionNoColonLayout := "2006-01-02T15:04Z0700" 50 | // Minutes precision with 2-digit tz-offset 51 | minPrecisionTZ2Layout := "2006-01-02T15:04Z07" 52 | 53 | // "Dialog" format. 54 | // yyyy-MM-dd hh:mm:ss (no tz-offset) 55 | dialogLayout := "2006-01-02 15:04:05" 56 | 57 | // Register our parsing layouts with the strfmt package. 58 | strfmt.DateTimeFormats = 59 | append(strfmt.DateTimeFormats, 60 | rfc3339TZ2Layout, 61 | secsPrecisionLayout, 62 | secsPrecisionNoColonLayout, 63 | secsPrecisionTZ2Layout, 64 | minPrecisionLayout, 65 | minPrecisionNoColonLayout, 66 | minPrecisionTZ2Layout, 67 | dialogLayout) 68 | } 69 | 70 | // NormalizeDateTimeUTC normalizes t to reflect UTC timezone for marshaling 71 | func NormalizeDateTimeUTC(t time.Time) time.Time { 72 | return t.UTC() 73 | } 74 | 75 | // ParseDate parses the specified RFC3339 full-date string (YYYY-MM-DD) and returns a strfmt.Date instance. 76 | // If the string is empty the return value will be the unix epoch (1970-01-01). 77 | func ParseDate(dateString string) (fmtDate strfmt.Date, err error) { 78 | if dateString == "" { 79 | return strfmt.Date(time.Unix(0, 0).UTC()), nil 80 | } 81 | 82 | formattedTime, err := time.Parse(strfmt.RFC3339FullDate, dateString) 83 | if err == nil { 84 | fmtDate = strfmt.Date(formattedTime) 85 | } else { 86 | err = SDKErrorf(err, "", "date-parse-error", getComponentInfo()) 87 | } 88 | return 89 | } 90 | 91 | // ParseDateTime parses the specified date-time string and returns a strfmt.DateTime instance. 92 | // If the string is empty the return value will be the unix epoch (1970-01-01T00:00:00.000Z). 93 | func ParseDateTime(dateString string) (strfmt.DateTime, error) { 94 | dt, err := strfmt.ParseDateTime(dateString) 95 | if err != nil { 96 | err = SDKErrorf(err, "", "datetime-parse-error", getComponentInfo()) 97 | } 98 | return dt, err 99 | } 100 | -------------------------------------------------------------------------------- /core/datetime_test.go: -------------------------------------------------------------------------------- 1 | //go:build all || fast 2 | 3 | package core 4 | 5 | /** 6 | * (C) Copyright IBM Corp. 2020. 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | 21 | import ( 22 | "encoding/json" 23 | "testing" 24 | "time" 25 | 26 | "github.com/go-openapi/strfmt" 27 | "github.com/stretchr/testify/assert" 28 | ) 29 | 30 | func _testDateTime(t *testing.T, src string, expected string) { 31 | // parse "src" into a DateTime value.package core 32 | dt, err := strfmt.ParseDateTime(src) 33 | assert.Nil(t, err) 34 | 35 | // format the DateTime value and verify. 36 | actual := dt.String() 37 | assert.NotNil(t, actual) 38 | 39 | assert.Equal(t, expected, actual) 40 | } 41 | 42 | func TestDateTime(t *testing.T) { 43 | // RFC 3339 with various flavors of tz-offset 44 | _testDateTime(t, "2016-06-20T04:25:16.218Z", "2016-06-20T04:25:16.218Z") 45 | _testDateTime(t, "2016-06-20T04:25:16.218+0000", "2016-06-20T04:25:16.218Z") 46 | _testDateTime(t, "2016-06-20T04:25:16.218+00", "2016-06-20T04:25:16.218Z") 47 | _testDateTime(t, "2016-06-20T05:25:16.218+01", "2016-06-20T04:25:16.218Z") 48 | _testDateTime(t, "2016-06-20T04:25:16.218-0000", "2016-06-20T04:25:16.218Z") 49 | _testDateTime(t, "2016-06-20T04:25:16.218-00", "2016-06-20T04:25:16.218Z") 50 | _testDateTime(t, "2016-06-20T00:25:16.218-0400", "2016-06-20T04:25:16.218Z") 51 | _testDateTime(t, "2016-06-20T00:25:16.218-04", "2016-06-20T04:25:16.218Z") 52 | _testDateTime(t, "2016-06-20T07:25:16.218+0300", "2016-06-20T04:25:16.218Z") 53 | _testDateTime(t, "2016-06-20T07:25:16.218+03", "2016-06-20T04:25:16.218Z") 54 | _testDateTime(t, "2016-06-20T04:25:16Z", "2016-06-20T04:25:16.000Z") 55 | _testDateTime(t, "2016-06-20T01:25:16-0300", "2016-06-20T04:25:16.000Z") 56 | _testDateTime(t, "2016-06-20T01:25:16-03:00", "2016-06-20T04:25:16.000Z") 57 | _testDateTime(t, "2016-06-20T08:55:16+04:30", "2016-06-20T04:25:16.000Z") 58 | _testDateTime(t, "2016-06-20T16:25:16+12:00", "2016-06-20T04:25:16.000Z") 59 | 60 | // RFC 3339 with nanoseconds for the Catalog-Managements of the world. 61 | _testDateTime(t, "2020-03-12T10:52:12.866305005-04:00", "2020-03-12T14:52:12.866Z") 62 | _testDateTime(t, "2020-03-12T14:52:12.866305005Z", "2020-03-12T14:52:12.866Z") 63 | _testDateTime(t, "2020-03-12T16:52:12.866305005+02:30", "2020-03-12T14:22:12.866Z") 64 | _testDateTime(t, "2020-03-12T14:52:12.866305Z", "2020-03-12T14:52:12.866Z") 65 | 66 | // UTC datetime with no TZ. 67 | _testDateTime(t, "2016-06-20T04:25:16.218", "2016-06-20T04:25:16.218Z") 68 | _testDateTime(t, "2016-06-20T04:25:16", "2016-06-20T04:25:16.000Z") 69 | 70 | // Dialog datetime. 71 | _testDateTime(t, "2016-06-20 04:25:16", "2016-06-20T04:25:16.000Z") 72 | 73 | // Alchemy datetime. 74 | // _testDateTime(t, "20160620T042516", "2016-06-20T04:25:16.000Z") 75 | 76 | // IAM Identity Service. 77 | _testDateTime(t, "2020-11-10T12:28+0000", "2020-11-10T12:28:00.000Z") 78 | _testDateTime(t, "2020-11-10T07:28-0500", "2020-11-10T12:28:00.000Z") 79 | _testDateTime(t, "2020-11-10T12:28Z", "2020-11-10T12:28:00.000Z") 80 | } 81 | 82 | type DateTimeModel struct { 83 | WsVictory *strfmt.DateTime `json:"ws_victory"` 84 | } 85 | 86 | type DateModel struct { 87 | WsVictory *strfmt.Date `json:"ws_victory"` 88 | } 89 | 90 | func roundTripTestDate(t *testing.T, inputJSON string, expectedOutputJSON string) { 91 | 92 | // Unmarshal inputJSON into a DateTimeModel instance 93 | var dModel *DateModel = nil 94 | err := json.Unmarshal([]byte(inputJSON), &dModel) 95 | assert.Nil(t, err) 96 | 97 | // Now marshal the model instance and verify the resulting JSON string. 98 | buf, err := json.Marshal(dModel) 99 | assert.Nil(t, err) 100 | actualOutputJSON := string(buf) 101 | 102 | t.Logf("Date input: %s, output: %s\n", inputJSON, actualOutputJSON) 103 | assert.Equal(t, expectedOutputJSON, actualOutputJSON) 104 | } 105 | func roundTripTestDateTime(t *testing.T, inputJSON string, expectedOutputJSON string) { 106 | 107 | // Unmarshal inputJSON into a DateTimeModel instance 108 | var dtModel *DateTimeModel = nil 109 | err := json.Unmarshal([]byte(inputJSON), &dtModel) 110 | assert.Nil(t, err) 111 | 112 | // Now marshal the model instance and verify the resulting JSON string. 113 | buf, err := json.Marshal(dtModel) 114 | assert.Nil(t, err) 115 | actualOutputJSON := string(buf) 116 | 117 | t.Logf("DateTime input: %s, output: %s\n", inputJSON, actualOutputJSON) 118 | assert.Equal(t, expectedOutputJSON, actualOutputJSON) 119 | } 120 | 121 | func TestModelsDateTime(t *testing.T) { 122 | // RFC 3339 date-time with milliseconds with Z tz-offset. 123 | roundTripTestDateTime(t, `{"ws_victory":"1903-10-13T21:30:00.000Z"}`, `{"ws_victory":"1903-10-13T21:30:00.000Z"}`) 124 | roundTripTestDateTime(t, `{"ws_victory":"1903-10-13T21:30:00.00011Z"}`, `{"ws_victory":"1903-10-13T21:30:00.000Z"}`) 125 | roundTripTestDateTime(t, `{"ws_victory":"1903-10-13T21:30:00.0001134Z"}`, `{"ws_victory":"1903-10-13T21:30:00.000Z"}`) 126 | roundTripTestDateTime(t, `{"ws_victory":"1903-10-13T21:30:00.000113456Z"}`, `{"ws_victory":"1903-10-13T21:30:00.000Z"}`) 127 | 128 | // RFC 3339 date-time without milliseconds with Z tz-offset. 129 | roundTripTestDateTime(t, `{"ws_victory":"1912-10-16T19:34:00Z"}`, `{"ws_victory":"1912-10-16T19:34:00.000Z"}`) 130 | 131 | // RFC 3339 date-time with milliseconds with non-Z tz-offset. 132 | roundTripTestDateTime(t, `{"ws_victory":"1915-10-13T16:15:00.000-03:00"}`, `{"ws_victory":"1915-10-13T19:15:00.000Z"}`) 133 | roundTripTestDateTime(t, `{"ws_victory":"1915-10-13T22:15:00.000+0300"}`, `{"ws_victory":"1915-10-13T19:15:00.000Z"}`) 134 | roundTripTestDateTime(t, `{"ws_victory":"1915-10-13T16:15:00.000-03"}`, `{"ws_victory":"1915-10-13T19:15:00.000Z"}`) 135 | roundTripTestDateTime(t, `{"ws_victory":"1915-10-13T22:15:00.000+03"}`, `{"ws_victory":"1915-10-13T19:15:00.000Z"}`) 136 | 137 | // RFC 3339 date-time without milliseconds with non-Z tz-offset. 138 | roundTripTestDateTime(t, `{"ws_victory":"1916-10-12T13:43:00-05:00"}`, `{"ws_victory":"1916-10-12T18:43:00.000Z"}`) 139 | roundTripTestDateTime(t, `{"ws_victory":"1916-10-12T13:43:00-05"}`, `{"ws_victory":"1916-10-12T18:43:00.000Z"}`) 140 | roundTripTestDateTime(t, `{"ws_victory":"1916-10-12T21:13:00+0230"}`, `{"ws_victory":"1916-10-12T18:43:00.000Z"}`) 141 | 142 | // RFC 3339 with nanoseconds for the Catalog-Managements of the world. 143 | roundTripTestDateTime(t, `{"ws_victory":"1916-10-12T13:43:00.866305005-05:00"}`, `{"ws_victory":"1916-10-12T18:43:00.866Z"}`) 144 | 145 | // UTC date-time with no tz. 146 | roundTripTestDateTime(t, `{"ws_victory":"1918-09-11T19:06:00.000"}`, `{"ws_victory":"1918-09-11T19:06:00.000Z"}`) 147 | roundTripTestDateTime(t, `{"ws_victory":"1918-09-11T19:06:00"}`, `{"ws_victory":"1918-09-11T19:06:00.000Z"}`) 148 | 149 | // Dialog date-time. 150 | roundTripTestDateTime(t, `{"ws_victory":"2004-10-28 04:39:00"}`, `{"ws_victory":"2004-10-28T04:39:00.000Z"}`) 151 | } 152 | 153 | func TestModelsDate(t *testing.T) { 154 | roundTripTestDate(t, `{"ws_victory":"1903-10-13"}`, `{"ws_victory":"1903-10-13"}`) 155 | roundTripTestDate(t, `{"ws_victory":"1912-10-16"}`, `{"ws_victory":"1912-10-16"}`) 156 | roundTripTestDate(t, `{"ws_victory":"1915-10-13"}`, `{"ws_victory":"1915-10-13"}`) 157 | roundTripTestDate(t, `{"ws_victory":"1916-10-12"}`, `{"ws_victory":"1916-10-12"}`) 158 | roundTripTestDate(t, `{"ws_victory":"1918-09-11"}`, `{"ws_victory":"1918-09-11"}`) 159 | roundTripTestDate(t, `{"ws_victory":"2004-10-28"}`, `{"ws_victory":"2004-10-28"}`) 160 | roundTripTestDate(t, `{"ws_victory":"2007-10-29"}`, `{"ws_victory":"2007-10-29"}`) 161 | roundTripTestDate(t, `{"ws_victory":"2013-10-31"}`, `{"ws_victory":"2013-10-31"}`) 162 | roundTripTestDate(t, `{"ws_victory":"2018-10-29"}`, `{"ws_victory":"2018-10-29"}`) 163 | } 164 | 165 | func TestDateTimeUtil(t *testing.T) { 166 | dateVar := strfmt.Date(time.Now()) 167 | fmtDate, err := ParseDate(dateVar.String()) 168 | assert.Nil(t, err) 169 | assert.Equal(t, dateVar.String(), fmtDate.String()) 170 | 171 | fmtDate, err = ParseDate("not a date") 172 | assert.Equal(t, strfmt.Date{}, fmtDate) 173 | assert.NotNil(t, err) 174 | 175 | fmtDate, err = ParseDate("") 176 | assert.Equal(t, strfmt.Date(time.Unix(0, 0).UTC()), fmtDate) 177 | assert.Nil(t, err) 178 | 179 | dateTimeVar := strfmt.DateTime(time.Now()) 180 | var fmtDTime strfmt.DateTime 181 | fmtDTime, err = ParseDateTime(dateTimeVar.String()) 182 | assert.Nil(t, err) 183 | assert.Equal(t, dateTimeVar.String(), fmtDTime.String()) 184 | 185 | fmtDTime, err = ParseDateTime("not a datetime") 186 | assert.Equal(t, strfmt.DateTime{}, fmtDTime) 187 | assert.NotNil(t, err) 188 | 189 | fmtDTime, err = ParseDateTime("") 190 | assert.Equal(t, strfmt.NewDateTime(), fmtDTime) 191 | assert.Nil(t, err) 192 | } 193 | -------------------------------------------------------------------------------- /core/detailed_response.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | // (C) Copyright IBM Corp. 2019, 2024. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | import ( 18 | "encoding/json" 19 | "fmt" 20 | "net/http" 21 | ) 22 | 23 | // DetailedResponse holds the response information received from the server. 24 | type DetailedResponse struct { 25 | 26 | // The HTTP status code associated with the response. 27 | StatusCode int `yaml:"status_code"` 28 | 29 | // The HTTP headers contained in the response. 30 | Headers http.Header `yaml:"headers"` 31 | 32 | // Result - this field will contain the result of the operation (obtained from the response body). 33 | // 34 | // If the operation was successful and the response body contains a JSON response, it is un-marshalled 35 | // into an object of the appropriate type (defined by the particular operation), and the Result field will contain 36 | // this response object. If there was an error while un-marshalling the JSON response body, then the RawResult field 37 | // will be set to the byte array containing the response body. 38 | // 39 | // Alternatively, if the generated SDK code passes in a result object which is an io.ReadCloser instance, 40 | // the JSON un-marshalling step is bypassed and the response body is simply returned in the Result field. 41 | // This scenario would occur in a situation where the SDK would like to provide a streaming model for large JSON 42 | // objects. 43 | // 44 | // If the operation was successful and the response body contains a non-JSON response, 45 | // the Result field will be an instance of io.ReadCloser that can be used by generated SDK code 46 | // (or the application) to read the response data. 47 | // 48 | // If the operation was unsuccessful and the response body contains a JSON error response, 49 | // this field will contain an instance of map[string]interface{} which is the result of un-marshalling the 50 | // response body as a "generic" JSON object. 51 | // If the JSON response for an unsuccessful operation could not be properly un-marshalled, then the 52 | // RawResult field will contain the raw response body. 53 | Result interface{} `yaml:"result,omitempty"` 54 | 55 | // This field will contain the raw response body as a byte array under these conditions: 56 | // 1) there was a problem un-marshalling a JSON response body - 57 | // either for a successful or unsuccessful operation. 58 | // 2) the operation was unsuccessful, and the response body contains a non-JSON response. 59 | RawResult []byte `yaml:"raw_result,omitempty"` 60 | } 61 | 62 | // GetHeaders returns the headers 63 | func (response *DetailedResponse) GetHeaders() http.Header { 64 | return response.Headers 65 | } 66 | 67 | // GetStatusCode returns the HTTP status code 68 | func (response *DetailedResponse) GetStatusCode() int { 69 | return response.StatusCode 70 | } 71 | 72 | // GetResult returns the result from the service 73 | func (response *DetailedResponse) GetResult() interface{} { 74 | return response.Result 75 | } 76 | 77 | // GetResultAsMap returns the result as a map (generic JSON object), if the 78 | // DetailedResponse.Result field contains an instance of a map. 79 | func (response *DetailedResponse) GetResultAsMap() (map[string]interface{}, bool) { 80 | m, ok := response.Result.(map[string]interface{}) 81 | return m, ok 82 | } 83 | 84 | // GetRawResult returns the raw response body as a byte array. 85 | func (response *DetailedResponse) GetRawResult() []byte { 86 | return response.RawResult 87 | } 88 | 89 | func (response *DetailedResponse) String() string { 90 | output, err := json.MarshalIndent(response, "", " ") 91 | if err == nil { 92 | return fmt.Sprintf("%+v\n", string(output)) 93 | } 94 | return fmt.Sprintf("Error marshalling DetailedResponse instance: %s", err.Error()) 95 | } 96 | -------------------------------------------------------------------------------- /core/detailed_response_test.go: -------------------------------------------------------------------------------- 1 | //go:build all || fast || basesvc 2 | 3 | package core 4 | 5 | // (C) Copyright IBM Corp. 2019. 6 | // 7 | // Licensed under the Apache License, Version 2.0 (the "License"); 8 | // you may not use this file except in compliance with the License. 9 | // You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, software 14 | // distributed under the License is distributed on an "AS IS" BASIS, 15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | // See the License for the specific language governing permissions and 17 | // limitations under the License. 18 | 19 | import ( 20 | "net/http" 21 | "testing" 22 | 23 | "github.com/stretchr/testify/assert" 24 | ) 25 | 26 | type TestStructure struct { 27 | Name string `json:"name"` 28 | } 29 | 30 | func TestDetailedResponseJsonSuccess(t *testing.T) { 31 | testStructure := TestStructure{ 32 | Name: "wonder woman", 33 | } 34 | 35 | headers := http.Header{} 36 | headers.Add("Content-Type", "application/json") 37 | 38 | response := &DetailedResponse{ 39 | StatusCode: 200, 40 | Result: testStructure, 41 | Headers: headers, 42 | } 43 | assert.Equal(t, 200, response.GetStatusCode()) 44 | assert.Equal(t, "application/json", response.GetHeaders().Get("Content-Type")) 45 | assert.Equal(t, testStructure, response.GetResult()) 46 | assert.Nil(t, response.GetRawResult()) 47 | m, ok := response.GetResultAsMap() 48 | assert.Equal(t, false, ok) 49 | assert.Nil(t, m) 50 | 51 | s := response.String() 52 | assert.NotEmpty(t, s) 53 | t.Logf("detailed response:\n%s", s) 54 | } 55 | 56 | func TestDetailedResponseNonJson(t *testing.T) { 57 | responseBody := []byte(`This is a non-json response body.`) 58 | 59 | headers := http.Header{} 60 | headers.Add("Content-Type", "application/octet-stream") 61 | 62 | response := &DetailedResponse{ 63 | StatusCode: 200, 64 | RawResult: responseBody, 65 | Headers: headers, 66 | } 67 | assert.Equal(t, 200, response.GetStatusCode()) 68 | assert.Equal(t, "application/octet-stream", response.GetHeaders().Get("Content-Type")) 69 | assert.Equal(t, responseBody, response.GetRawResult()) 70 | assert.Nil(t, response.GetResult()) 71 | m, ok := response.GetResultAsMap() 72 | assert.Equal(t, false, ok) 73 | assert.Nil(t, m) 74 | } 75 | 76 | func TestDetailedResponseJsonMap(t *testing.T) { 77 | errorMap := make(map[string]interface{}) 78 | errorMap["message"] = "An error message." 79 | 80 | headers := http.Header{} 81 | headers.Add("Content-Type", "application/json") 82 | 83 | response := &DetailedResponse{ 84 | StatusCode: 400, 85 | Result: errorMap, 86 | Headers: headers, 87 | } 88 | assert.Equal(t, 400, response.GetStatusCode()) 89 | assert.Equal(t, "application/json", response.GetHeaders().Get("Content-Type")) 90 | m, ok := response.GetResultAsMap() 91 | assert.Equal(t, true, ok) 92 | assert.Equal(t, errorMap, m) 93 | assert.Nil(t, response.GetRawResult()) 94 | } 95 | -------------------------------------------------------------------------------- /core/doc.go: -------------------------------------------------------------------------------- 1 | // (C) Copyright IBM Corp. 2019. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /* 16 | Package core contains functionality used by Go SDK's generated by the IBM 17 | OpenAPI 3 SDK Generator (openapi-sdkgen). 18 | Authenticators 19 | 20 | The go-sdk-core project supports the following types of authentication: 21 | 22 | Basic Authentication 23 | Bearer Token 24 | Identity and Access Management (IAM) 25 | Cloud Pak for Data 26 | No Authentication 27 | 28 | The authentication types that are appropriate for a particular service may 29 | vary from service to service. Each authentication type is implemented as an 30 | Authenticator for consumption by a service. To read more about authenticators 31 | and how to use them see here: 32 | https://github.com/IBM/go-sdk-core/blob/main/Authentication.md 33 | 34 | # Services 35 | 36 | Services are the API clients generated by the IBM OpenAPI 3 SDK 37 | Generator. These services make use of the code within the core package 38 | BaseService instances to perform service operations. 39 | */ 40 | package core 41 | -------------------------------------------------------------------------------- /core/file_with_metadata.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | // (C) Copyright IBM Corp. 2021. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | import ( 18 | "encoding/json" 19 | "io" 20 | "os" 21 | "reflect" 22 | ) 23 | 24 | // FileWithMetadata : A file with its associated metadata. 25 | type FileWithMetadata struct { 26 | // The data / content for the file. 27 | Data io.ReadCloser `json:"data" validate:"required"` 28 | 29 | // The filename of the file. 30 | Filename *string `json:"filename,omitempty"` 31 | 32 | // The content type of the file. 33 | ContentType *string `json:"content_type,omitempty"` 34 | } 35 | 36 | // NewFileWithMetadata : Instantiate FileWithMetadata (Generic Model Constructor) 37 | func NewFileWithMetadata(data io.ReadCloser) (model *FileWithMetadata, err error) { 38 | model = &FileWithMetadata{ 39 | Data: data, 40 | } 41 | err = RepurposeSDKProblem(ValidateStruct(model, "required parameters"), "validation-failed") 42 | return 43 | } 44 | 45 | // UnmarshalFileWithMetadata unmarshals an instance of FileWithMetadata from the specified map of raw messages. 46 | // The "data" field is assumed to be a string, the value of which is assumed to be a path to the file that 47 | // contains the data intended for the FileWithMetadata struct. 48 | func UnmarshalFileWithMetadata(m map[string]json.RawMessage, result interface{}) (err error) { 49 | obj := new(FileWithMetadata) 50 | 51 | // unmarshal the data field as a filename and read the contents 52 | // then explicitly set the Data field to the contents of the file 53 | var data io.ReadCloser 54 | var pathToData string 55 | err = RepurposeSDKProblem(UnmarshalPrimitive(m, "data", &pathToData), "unmarshal-fail") 56 | if err != nil { 57 | return 58 | } 59 | data, err = os.Open(pathToData) // #nosec G304 60 | if err != nil { 61 | err = SDKErrorf(err, "", "file-open-error", getComponentInfo()) 62 | return 63 | } 64 | obj.Data = data 65 | 66 | // unmarshal the other fields as usual 67 | err = RepurposeSDKProblem(UnmarshalPrimitive(m, "filename", &obj.Filename), "unmarshal-file-fail") 68 | if err != nil { 69 | return 70 | } 71 | err = RepurposeSDKProblem(UnmarshalPrimitive(m, "content_type", &obj.ContentType), "unmarshal-content-type-fail") 72 | if err != nil { 73 | return 74 | } 75 | reflect.ValueOf(result).Elem().Set(reflect.ValueOf(obj)) 76 | return 77 | } 78 | -------------------------------------------------------------------------------- /core/file_with_metadata_test.go: -------------------------------------------------------------------------------- 1 | //go:build all || fast 2 | 3 | package core 4 | 5 | // (C) Copyright IBM Corp. 2021, 2024. 6 | // 7 | // Licensed under the Apache License, Version 2.0 (the "License"); 8 | // you may not use this file except in compliance with the License. 9 | // You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, software 14 | // distributed under the License is distributed on an "AS IS" BASIS, 15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | // See the License for the specific language governing permissions and 17 | // limitations under the License. 18 | 19 | import ( 20 | "bytes" 21 | "encoding/json" 22 | "io" 23 | "os" 24 | "testing" 25 | 26 | "github.com/stretchr/testify/assert" 27 | ) 28 | 29 | func TestFileWithMetadataFields(t *testing.T) { 30 | data := io.NopCloser(bytes.NewReader([]byte("test"))) 31 | filename := "test.txt" 32 | contentType := "application/octet-stream" 33 | 34 | model := FileWithMetadata{ 35 | Data: data, 36 | Filename: &filename, 37 | ContentType: &contentType, 38 | } 39 | 40 | assert.NotNil(t, model.Data) 41 | assert.NotNil(t, model.Filename) 42 | assert.NotNil(t, model.ContentType) 43 | } 44 | 45 | func TestNewFileWithMetadata(t *testing.T) { 46 | data := io.NopCloser(bytes.NewReader([]byte("test"))) 47 | model, err := NewFileWithMetadata(data) 48 | 49 | assert.Nil(t, err) 50 | myData := model.Data 51 | assert.NotNil(t, myData) 52 | 53 | assert.Nil(t, model.Filename) 54 | assert.Nil(t, model.ContentType) 55 | } 56 | 57 | func TestUnmarshalFileWithMetadata(t *testing.T) { 58 | var err error 59 | 60 | // setup the test by creating a temp directory and file for the unmarshaler to read 61 | err = os.Mkdir("tempdir", 0755) 62 | assert.Nil(t, err) 63 | 64 | message := []byte("test") 65 | err = os.WriteFile("tempdir/test-file.txt", message, 0644) // #nosec:G306 66 | assert.Nil(t, err) 67 | 68 | // mock what user input would look like - a map converted from a JSON string 69 | exampleJsonString := `{"data": "tempdir/test-file.txt", "filename": "test-file.txt", "content_type": "text/plain"}` 70 | 71 | var mapifiedString map[string]json.RawMessage 72 | err = json.Unmarshal([]byte(exampleJsonString), &mapifiedString) 73 | assert.Nil(t, err) 74 | 75 | var model *FileWithMetadata 76 | 77 | err = UnmarshalFileWithMetadata(mapifiedString, &model) 78 | assert.Nil(t, err) 79 | 80 | data := model.Data 81 | assert.NotNil(t, data) 82 | 83 | assert.NotNil(t, model.Filename) 84 | assert.Equal(t, "test-file.txt", *model.Filename) 85 | 86 | assert.NotNil(t, model.ContentType) 87 | assert.Equal(t, "text/plain", *model.ContentType) 88 | 89 | err = os.RemoveAll("tempdir") 90 | assert.Nil(t, err) 91 | } 92 | -------------------------------------------------------------------------------- /core/gzip.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | // (C) Copyright IBM Corp. 2020. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | import ( 18 | "compress/gzip" 19 | "io" 20 | ) 21 | 22 | // NewGzipCompressionReader will return an io.Reader instance that will deliver 23 | // the gzip-compressed version of the "uncompressedReader" argument. 24 | // This function was inspired by this github gist: 25 | // 26 | // https://gist.github.com/tomcatzh/cf8040820962e0f8c04700eb3b2f26be 27 | func NewGzipCompressionReader(uncompressedReader io.Reader) (io.Reader, error) { 28 | // Create a pipe whose reader will effectively replace "uncompressedReader" 29 | // to deliver the gzip-compressed byte stream. 30 | pipeReader, pipeWriter := io.Pipe() 31 | go func() { 32 | defer pipeWriter.Close() 33 | 34 | // Wrap the pipe's writer with a gzip writer that will 35 | // write the gzip-compressed bytes to the Pipe. 36 | compressedWriter := gzip.NewWriter(pipeWriter) 37 | defer compressedWriter.Close() 38 | 39 | // To trigger the operation of the pipe, we'll simply start 40 | // to copy bytes from "uncompressedReader" to "compressedWriter". 41 | // This copy operation will block as needed in order to write bytes 42 | // to the pipe only when the pipe reader is called to retrieve more bytes. 43 | _, err := io.Copy(compressedWriter, uncompressedReader) 44 | if err != nil { 45 | sdkErr := SDKErrorf(err, "", "compression-failed", getComponentInfo()) 46 | _ = pipeWriter.CloseWithError(sdkErr) 47 | } 48 | }() 49 | return pipeReader, nil 50 | } 51 | 52 | // NewGzipDecompressionReader will return an io.Reader instance that will deliver 53 | // the gzip-decompressed version of the "compressedReader" argument. 54 | func NewGzipDecompressionReader(compressedReader io.Reader) (io.Reader, error) { 55 | res, err := gzip.NewReader(compressedReader) 56 | if err != nil { 57 | err = SDKErrorf(err, "", "decompress-read-error", getComponentInfo()) 58 | } 59 | return res, err 60 | } 61 | -------------------------------------------------------------------------------- /core/gzip_test.go: -------------------------------------------------------------------------------- 1 | //go:build all || fast || basesvc 2 | 3 | package core 4 | 5 | // (C) Copyright IBM Corp. 2020. 6 | // 7 | // Licensed under the Apache License, Version 2.0 (the "License"); 8 | // you may not use this file except in compliance with the License. 9 | // You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, software 14 | // distributed under the License is distributed on an "AS IS" BASIS, 15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | // See the License for the specific language governing permissions and 17 | // limitations under the License. 18 | 19 | import ( 20 | "bytes" 21 | "testing" 22 | 23 | "github.com/stretchr/testify/assert" 24 | ) 25 | 26 | func testRoundTripBytes(t *testing.T, src []byte) { 27 | // Compress the input string and store in a buffer. 28 | srcReader := bytes.NewReader(src) 29 | gzipCompressor, err := NewGzipCompressionReader(srcReader) 30 | assert.Nil(t, err) 31 | compressedBuf := new(bytes.Buffer) 32 | _, err = compressedBuf.ReadFrom(gzipCompressor) 33 | assert.Nil(t, err) 34 | t.Log("Compressed length: ", compressedBuf.Len()) 35 | 36 | // Now uncompress the compressed bytes and store in another buffer. 37 | bytesReader := bytes.NewReader(compressedBuf.Bytes()) 38 | gzipDecompressor, err := NewGzipDecompressionReader(bytesReader) 39 | assert.Nil(t, err) 40 | decompressedBuf := new(bytes.Buffer) 41 | _, err = decompressedBuf.ReadFrom(gzipDecompressor) 42 | assert.Nil(t, err) 43 | t.Log("Uncompressed length: ", decompressedBuf.Len()) 44 | 45 | // Verify that the uncompressed bytes produce the original string. 46 | assert.Equal(t, src, decompressedBuf.Bytes()) 47 | } 48 | func TestGzipCompressionString1(t *testing.T) { 49 | testRoundTripBytes(t, []byte("Hello world!")) 50 | } 51 | 52 | func TestGzipCompressionString2(t *testing.T) { 53 | s := "This is a somewhat longer string, which we'll try to use in our compression/decompression testing. Hopefully this will workout ok, but who knows???" 54 | testRoundTripBytes(t, []byte(s)) 55 | } 56 | 57 | func TestGzipCompressionString3(t *testing.T) { 58 | s := "This is a string that should be able to be compressed by a LOT......................................................................................................................................................................................................................................................................................................................................................." 59 | testRoundTripBytes(t, []byte(s)) 60 | } 61 | 62 | func TestGzipCompressionJSON1(t *testing.T) { 63 | jsonString := `{ 64 | "rules": [ 65 | { 66 | "request_id": "request-0", 67 | "rule": { 68 | "account_id": "44890a2fd24641a5a111738e358686cc", 69 | "name": "Go Test Rule #1", 70 | "description": "This is the description for Go Test Rule #1.", 71 | "rule_type": "user_defined", 72 | "target": { 73 | "service_name": "config-gov-sdk-integration-test-service", 74 | "resource_kind": "bucket", 75 | "additional_target_attributes": [ 76 | { 77 | "name": "resource_id", 78 | "operator": "is_not_empty" 79 | } 80 | ] 81 | }, 82 | "required_config": { 83 | "description": "allowed_gb\u003c=20 \u0026\u0026 location=='us-east'", 84 | "and": [ 85 | { 86 | "property": "allowed_gb", 87 | "operator": "num_less_than_equals", 88 | "value": "20" 89 | }, 90 | { 91 | "property": "location", 92 | "operator": "string_equals", 93 | "value": "us-east" 94 | } 95 | ] 96 | }, 97 | "enforcement_actions": [ 98 | { 99 | "action": "disallow" 100 | } 101 | ], 102 | "labels": [ 103 | "GoSDKIntegrationTest" 104 | ] 105 | } 106 | } 107 | ], 108 | "Transaction-Id": "bb5bac98-fa55-4125-97a8-578811c39c81", 109 | "Headers": null 110 | }` 111 | 112 | testRoundTripBytes(t, []byte(jsonString)) 113 | } 114 | 115 | func TestGzipCompressionJSON2(t *testing.T) { 116 | s := make([]string, 0) 117 | 118 | // Create a large string slice with repeated values, which will result in a small compressed string. 119 | for i := 0; i < 100000; i++ { 120 | s = append(s, "This") 121 | s = append(s, "is") 122 | s = append(s, "a") 123 | s = append(s, "test") 124 | s = append(s, "that ") 125 | s = append(s, "should") 126 | s = append(s, "demonstrate") 127 | s = append(s, "lots") 128 | s = append(s, "of") 129 | s = append(s, "compression") 130 | } 131 | 132 | jsonString := toJSON(s) 133 | 134 | testRoundTripBytes(t, []byte(jsonString)) 135 | } 136 | -------------------------------------------------------------------------------- /core/http_problem.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | // (C) Copyright IBM Corp. 2024. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | import ( 18 | "errors" 19 | "fmt" 20 | ) 21 | 22 | // HTTPProblem provides a type suited to problems that 23 | // occur as the result of an HTTP request. It extends 24 | // the base "IBMProblem" type with fields to store 25 | // information about the HTTP request/response. 26 | type HTTPProblem struct { 27 | *IBMProblem 28 | 29 | // OperationID identifies the operation of an API 30 | // that the failed request was made to. 31 | OperationID string 32 | 33 | // Response contains the full HTTP error response 34 | // returned as a result of the failed request, 35 | // including the body and all headers. 36 | Response *DetailedResponse 37 | } 38 | 39 | // GetConsoleMessage returns all public fields of 40 | // the problem, formatted in YAML. 41 | func (e *HTTPProblem) GetConsoleMessage() string { 42 | return ComputeConsoleMessage(e) 43 | } 44 | 45 | // GetDebugMessage returns all information about 46 | // the problem, formatted in YAML. 47 | func (e *HTTPProblem) GetDebugMessage() string { 48 | return ComputeDebugMessage(e) 49 | } 50 | 51 | // GetID returns the computed identifier, computed from the 52 | // "Component", "discriminator", and "OperationID" fields, 53 | // as well as the status code of the stored response and the 54 | // identifier of the "causedBy" problem, if it exists. 55 | func (e *HTTPProblem) GetID() string { 56 | // TODO: add the error code to the hash once we have the ability to enumerate error codes in an API. 57 | return CreateIDHash("http", e.GetBaseSignature(), e.OperationID, fmt.Sprint(e.Response.GetStatusCode())) 58 | } 59 | 60 | // Is allows an HTTPProblem instance to be compared against another error for equality. 61 | // An HTTPProblem is considered equal to another error if 1) the error is also a Problem and 62 | // 2) it has the same ID (i.e. it is the same problem scenario). 63 | func (e *HTTPProblem) Is(target error) bool { 64 | return is(target, e.GetID()) 65 | } 66 | 67 | func (e *HTTPProblem) getErrorCode() string { 68 | // If the error response was a standard JSON body, the result will 69 | // be a map and we can do a decent job of guessing the code. 70 | if e.Response.Result != nil { 71 | if resultMap, ok := e.Response.Result.(map[string]interface{}); ok { 72 | return getErrorCode(resultMap) 73 | } 74 | } 75 | 76 | return "" 77 | } 78 | 79 | func (e *HTTPProblem) getHeader(key string) (string, bool) { 80 | value := e.Response.Headers.Get(key) 81 | return value, value != "" 82 | } 83 | 84 | // GetConsoleOrderedMaps returns an ordered-map representation 85 | // of an HTTPProblem instance suited for a console message. 86 | func (e *HTTPProblem) GetConsoleOrderedMaps() *OrderedMaps { 87 | orderedMaps := NewOrderedMaps() 88 | 89 | orderedMaps.Add("id", e.GetID()) 90 | orderedMaps.Add("summary", e.Summary) 91 | orderedMaps.Add("severity", e.Severity) 92 | orderedMaps.Add("operation_id", e.OperationID) 93 | orderedMaps.Add("status_code", e.Response.GetStatusCode()) 94 | errorCode := e.getErrorCode() 95 | if errorCode != "" { 96 | orderedMaps.Add("error_code", errorCode) 97 | } 98 | orderedMaps.Add("component", e.Component) 99 | 100 | // Conditionally add the request ID and correlation ID header values. 101 | 102 | if header, ok := e.getHeader("x-request-id"); ok { 103 | orderedMaps.Add("request_id", header) 104 | } 105 | 106 | if header, ok := e.getHeader("x-correlation-id"); ok { 107 | orderedMaps.Add("correlation_id", header) 108 | } 109 | 110 | return orderedMaps 111 | } 112 | 113 | // GetDebugOrderedMaps returns an ordered-map representation 114 | // of an HTTPProblem instance, with additional information 115 | // suited for a debug message. 116 | func (e *HTTPProblem) GetDebugOrderedMaps() *OrderedMaps { 117 | orderedMaps := e.GetConsoleOrderedMaps() 118 | 119 | // The RawResult is never helpful in the printed message. Create a hard copy 120 | // (de-referenced pointer) to remove the raw result from so we don't alter 121 | // the response stored in the problem object. 122 | printableResponse := *e.Response 123 | if printableResponse.Result == nil { 124 | printableResponse.Result = string(printableResponse.RawResult) 125 | } 126 | printableResponse.RawResult = nil 127 | orderedMaps.Add("response", printableResponse) 128 | 129 | var orderableCausedBy OrderableProblem 130 | if errors.As(e.GetCausedBy(), &orderableCausedBy) { 131 | orderedMaps.Add("caused_by", orderableCausedBy.GetDebugOrderedMaps().GetMaps()) 132 | } 133 | 134 | return orderedMaps 135 | } 136 | 137 | // httpErrorf creates and returns a new instance of "HTTPProblem" with "error" level severity. 138 | func httpErrorf(summary string, response *DetailedResponse) *HTTPProblem { 139 | httpProb := &HTTPProblem{ 140 | IBMProblem: IBMErrorf(nil, NewProblemComponent("", ""), summary, ""), 141 | Response: response, 142 | } 143 | 144 | return httpProb 145 | } 146 | 147 | // EnrichHTTPProblem takes an problem and, if it originated as an HTTPProblem, populates 148 | // the fields of the underlying HTTP problem with the given service/operation information. 149 | func EnrichHTTPProblem(err error, operationID string, component *ProblemComponent) { 150 | // If the problem originated from an HTTP error response, populate the 151 | // HTTPProblem instance with details from the SDK that weren't available 152 | // in the core at problem creation time. 153 | var httpProb *HTTPProblem 154 | 155 | // In the case of an SDKProblem instance originating in the core, 156 | // it will not track an HTTPProblem instance in its "caused by" 157 | // chain, but we still want to be able to enrich it. It will be 158 | // stored in the private "httpProblem" field. 159 | var sdkProb *SDKProblem 160 | 161 | if errors.As(err, &httpProb) { 162 | enrichHTTPProblem(httpProb, operationID, component) 163 | } else if errors.As(err, &sdkProb) && sdkProb.httpProblem != nil { 164 | enrichHTTPProblem(sdkProb.httpProblem, operationID, component) 165 | } 166 | } 167 | 168 | // enrichHTTPProblem takes an HTTPProblem instance alongside information about the request 169 | // and adds the extra info to the instance. It also loosely deserializes the response 170 | // in order to set additional information, like the error code. 171 | func enrichHTTPProblem(httpProb *HTTPProblem, operationID string, component *ProblemComponent) { 172 | // If this problem is already populated with service-level information, 173 | // we should not enrich it any further. Most likely, this is an authentication 174 | // error passed from the core to the SDK. 175 | if httpProb.Component.Name != "" { 176 | return 177 | } 178 | 179 | httpProb.Component = component 180 | httpProb.OperationID = operationID 181 | } 182 | -------------------------------------------------------------------------------- /core/http_problem_test.go: -------------------------------------------------------------------------------- 1 | //go:build all || fast || problem 2 | 3 | package core 4 | 5 | // (C) Copyright IBM Corp. 2024. 6 | // 7 | // Licensed under the Apache License, Version 2.0 (the "License"); 8 | // you may not use this file except in compliance with the License. 9 | // You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, software 14 | // distributed under the License is distributed on an "AS IS" BASIS, 15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | // See the License for the specific language governing permissions and 17 | // limitations under the License. 18 | 19 | import ( 20 | "errors" 21 | "net/http" 22 | "testing" 23 | 24 | "github.com/stretchr/testify/assert" 25 | ) 26 | 27 | func TestHTTPProblemEmbedsIBMProblem(t *testing.T) { 28 | httpProb := &HTTPProblem{} 29 | 30 | // Check that the methods defined by IBMProblem are supported here. 31 | // The implementations are tested elsewhere. 32 | assert.NotNil(t, httpProb.Error) 33 | assert.NotNil(t, httpProb.GetBaseSignature) 34 | assert.NotNil(t, httpProb.GetCausedBy) 35 | assert.NotNil(t, httpProb.Unwrap) 36 | } 37 | 38 | func TestHTTPProblemGetConsoleMessage(t *testing.T) { 39 | httpProb := getPopulatedHTTPProblem() 40 | message := httpProb.GetConsoleMessage() 41 | expected := `--- 42 | id: http-1850a8fa 43 | summary: Bad request 44 | severity: error 45 | operation_id: create_resource 46 | status_code: 400 47 | error_code: invalid-input 48 | component: 49 | name: my-service 50 | version: v1 51 | request_id: abc123 52 | correlation_id: xyz789 53 | --- 54 | ` 55 | assert.Equal(t, expected, message) 56 | } 57 | 58 | func TestHTTPProblemGetDebugMessage(t *testing.T) { 59 | httpProb := getPopulatedHTTPProblem() 60 | message := httpProb.GetDebugMessage() 61 | expected := `--- 62 | id: http-1850a8fa 63 | summary: Bad request 64 | severity: error 65 | operation_id: create_resource 66 | status_code: 400 67 | error_code: invalid-input 68 | component: 69 | name: my-service 70 | version: v1 71 | request_id: abc123 72 | correlation_id: xyz789 73 | response: 74 | status_code: 400 75 | headers: 76 | Content-Type: 77 | - application/json 78 | X-Correlation-Id: 79 | - xyz789 80 | X-Request-Id: 81 | - abc123 82 | result: 83 | errorCode: invalid-input 84 | --- 85 | ` 86 | assert.Equal(t, expected, message) 87 | } 88 | 89 | func TestHTTPProblemGetID(t *testing.T) { 90 | httpProb := getPopulatedHTTPProblem() 91 | assert.Equal(t, "http-1850a8fa", httpProb.GetID()) 92 | } 93 | 94 | func TestHTTPProblemGetConsoleOrderedMaps(t *testing.T) { 95 | httpProb := getPopulatedHTTPProblem() 96 | orderedMaps := httpProb.GetConsoleOrderedMaps() 97 | assert.NotNil(t, orderedMaps) 98 | 99 | maps := orderedMaps.GetMaps() 100 | assert.NotNil(t, maps) 101 | assert.Len(t, maps, 9) 102 | 103 | assert.Equal(t, "id", maps[0].Key) 104 | assert.Equal(t, "http-1850a8fa", maps[0].Value) 105 | 106 | assert.Equal(t, "summary", maps[1].Key) 107 | assert.Equal(t, "Bad request", maps[1].Value) 108 | 109 | assert.Equal(t, "severity", maps[2].Key) 110 | assert.Equal(t, ErrorSeverity, maps[2].Value) 111 | 112 | assert.Equal(t, "operation_id", maps[3].Key) 113 | assert.Equal(t, "create_resource", maps[3].Value) 114 | 115 | assert.Equal(t, "status_code", maps[4].Key) 116 | assert.Equal(t, 400, maps[4].Value) 117 | 118 | assert.Equal(t, "error_code", maps[5].Key) 119 | assert.Equal(t, "invalid-input", maps[5].Value) 120 | 121 | assert.Equal(t, "component", maps[6].Key) 122 | assert.Equal(t, "my-service", maps[6].Value.(*ProblemComponent).Name) 123 | assert.Equal(t, "v1", maps[6].Value.(*ProblemComponent).Version) 124 | 125 | assert.Equal(t, "request_id", maps[7].Key) 126 | assert.Equal(t, "abc123", maps[7].Value) 127 | 128 | assert.Equal(t, "correlation_id", maps[8].Key) 129 | assert.Equal(t, "xyz789", maps[8].Value) 130 | } 131 | 132 | func TestHTTPProblemGetDebugOrderedMaps(t *testing.T) { 133 | httpProb := getPopulatedHTTPProblem() 134 | orderedMaps := httpProb.GetDebugOrderedMaps() 135 | assert.NotNil(t, orderedMaps) 136 | 137 | maps := orderedMaps.GetMaps() 138 | assert.NotNil(t, maps) 139 | assert.Len(t, maps, 10) 140 | 141 | assert.Equal(t, "id", maps[0].Key) 142 | assert.Equal(t, "http-1850a8fa", maps[0].Value) 143 | 144 | assert.Equal(t, "summary", maps[1].Key) 145 | assert.Equal(t, "Bad request", maps[1].Value) 146 | 147 | assert.Equal(t, "severity", maps[2].Key) 148 | assert.Equal(t, ErrorSeverity, maps[2].Value) 149 | 150 | assert.Equal(t, "operation_id", maps[3].Key) 151 | assert.Equal(t, "create_resource", maps[3].Value) 152 | 153 | assert.Equal(t, "status_code", maps[4].Key) 154 | assert.Equal(t, 400, maps[4].Value) 155 | 156 | assert.Equal(t, "error_code", maps[5].Key) 157 | assert.Equal(t, "invalid-input", maps[5].Value) 158 | 159 | assert.Equal(t, "component", maps[6].Key) 160 | assert.Equal(t, "my-service", maps[6].Value.(*ProblemComponent).Name) 161 | assert.Equal(t, "v1", maps[6].Value.(*ProblemComponent).Version) 162 | 163 | assert.Equal(t, "request_id", maps[7].Key) 164 | assert.Equal(t, "abc123", maps[7].Value) 165 | 166 | assert.Equal(t, "correlation_id", maps[8].Key) 167 | assert.Equal(t, "xyz789", maps[8].Value) 168 | 169 | assert.Equal(t, "response", maps[9].Key) 170 | assert.Equal(t, *getPopulatedDetailedResponse(), maps[9].Value) 171 | } 172 | 173 | func TestHTTPProblemGetDebugOrderedMapsWithoutOptionals(t *testing.T) { 174 | httpProb := getPopulatedHTTPProblemWithoutOptionals() 175 | orderedMaps := httpProb.GetDebugOrderedMaps() 176 | assert.NotNil(t, orderedMaps) 177 | 178 | maps := orderedMaps.GetMaps() 179 | assert.NotNil(t, maps) 180 | assert.Len(t, maps, 7) 181 | 182 | assert.Equal(t, "id", maps[0].Key) 183 | assert.Equal(t, "http-1850a8fa", maps[0].Value) 184 | 185 | assert.Equal(t, "summary", maps[1].Key) 186 | assert.Equal(t, "Bad request", maps[1].Value) 187 | 188 | assert.Equal(t, "severity", maps[2].Key) 189 | assert.Equal(t, ErrorSeverity, maps[2].Value) 190 | 191 | assert.Equal(t, "operation_id", maps[3].Key) 192 | assert.Equal(t, "create_resource", maps[3].Value) 193 | 194 | assert.Equal(t, "status_code", maps[4].Key) 195 | assert.Equal(t, 400, maps[4].Value) 196 | 197 | assert.Equal(t, "component", maps[5].Key) 198 | assert.Equal(t, "my-service", maps[5].Value.(*ProblemComponent).Name) 199 | assert.Equal(t, "v1", maps[5].Value.(*ProblemComponent).Version) 200 | 201 | assert.Equal(t, "response", maps[6].Key) 202 | assert.Equal(t, DetailedResponse{StatusCode: 400, Result: ""}, maps[6].Value) 203 | } 204 | 205 | func TestHTTPProblemGetHeader(t *testing.T) { 206 | httpProb := httpErrorf("Bad request", getPopulatedDetailedResponse()) 207 | val, ok := httpProb.getHeader("doesnt-exist") 208 | assert.Empty(t, val) 209 | assert.False(t, ok) 210 | 211 | val, ok = httpProb.getHeader("content-type") 212 | assert.Equal(t, "application/json", val) 213 | assert.True(t, ok) 214 | } 215 | 216 | func TestHTTPProblemGetErrorCodeEmpty(t *testing.T) { 217 | httpProb := httpErrorf("Bad request", &DetailedResponse{}) 218 | assert.Empty(t, httpProb.getErrorCode()) 219 | } 220 | 221 | func TestHTTPProblemGetErrorCode(t *testing.T) { 222 | httpProb := httpErrorf("Bad request", getPopulatedDetailedResponse()) 223 | assert.Equal(t, "invalid-input", httpProb.getErrorCode()) 224 | } 225 | 226 | func TestHTTPProblemIsWithProblem(t *testing.T) { 227 | firstProb := httpErrorf("Bad request", getPopulatedDetailedResponse()) 228 | EnrichHTTPProblem(firstProb, "create_resource", NewProblemComponent("service", "1.0.0")) 229 | 230 | secondProb := httpErrorf("Invalid input", getPopulatedDetailedResponse()) 231 | EnrichHTTPProblem(secondProb, "create_resource", NewProblemComponent("service", "1.2.3")) 232 | 233 | assert.NotEqual(t, firstProb, secondProb) 234 | assert.True(t, errors.Is(firstProb, secondProb)) 235 | } 236 | 237 | func TestHTTPErrorf(t *testing.T) { 238 | message := "Bad request" 239 | httpProb := httpErrorf(message, getPopulatedDetailedResponse()) 240 | 241 | // We don't have a lot of information about the request when we 242 | // create new HTTPProblem objects here in the core. 243 | assert.NotNil(t, httpProb) 244 | assert.Equal(t, message, httpProb.Summary) 245 | assert.Equal(t, getPopulatedDetailedResponse(), httpProb.Response) 246 | assert.Empty(t, httpProb.discriminator) 247 | assert.Empty(t, httpProb.Component) 248 | assert.Empty(t, httpProb.OperationID) 249 | assert.Nil(t, httpProb.causedBy) 250 | } 251 | 252 | func TestPublicEnrichHTTPProblem(t *testing.T) { 253 | err := httpErrorf("Bad request", &DetailedResponse{}) 254 | assert.Empty(t, err.Component) 255 | assert.Empty(t, err.OperationID) 256 | 257 | EnrichHTTPProblem(err, "delete_resource", NewProblemComponent("test", "v2")) 258 | 259 | assert.NotEmpty(t, err.Component) 260 | assert.Equal(t, "test", err.Component.Name) 261 | assert.Equal(t, "v2", err.Component.Version) 262 | assert.Equal(t, "delete_resource", err.OperationID) 263 | } 264 | 265 | func TestPublicEnrichHTTPProblemWithinSDKProblem(t *testing.T) { 266 | httpProb := httpErrorf("Bad request", &DetailedResponse{}) 267 | assert.Empty(t, httpProb.Component) 268 | assert.Empty(t, httpProb.OperationID) 269 | 270 | sdkProb := SDKErrorf(httpProb, "Wrong!", "", NewProblemComponent("sdk", "1.0.0")) 271 | EnrichHTTPProblem(sdkProb, "delete_resource", NewProblemComponent("test", "v2")) 272 | 273 | assert.NotEmpty(t, httpProb.Component) 274 | assert.Equal(t, "test", httpProb.Component.Name) 275 | assert.Equal(t, "v2", httpProb.Component.Version) 276 | assert.Equal(t, "delete_resource", httpProb.OperationID) 277 | } 278 | 279 | func TestPublicEnrichHTTPProblemWithinCoreProblem(t *testing.T) { 280 | httpProb := httpErrorf("Bad request", &DetailedResponse{}) 281 | assert.Empty(t, httpProb.Component) 282 | assert.Empty(t, httpProb.OperationID) 283 | 284 | sdkProb := SDKErrorf(httpProb, "Wrong!", "disc", getComponentInfo()) 285 | assert.Nil(t, sdkProb.causedBy) 286 | assert.NotNil(t, sdkProb.httpProblem) 287 | 288 | EnrichHTTPProblem(sdkProb, "delete_resource", NewProblemComponent("test", "v2")) 289 | 290 | assert.NotEmpty(t, httpProb.Component) 291 | assert.Equal(t, "test", httpProb.Component.Name) 292 | assert.Equal(t, "v2", httpProb.Component.Version) 293 | assert.Equal(t, "delete_resource", httpProb.OperationID) 294 | } 295 | 296 | func TestPrivateEnrichHTTPProblem(t *testing.T) { 297 | httpProb := httpErrorf("Bad request", &DetailedResponse{}) 298 | assert.Empty(t, httpProb.Component) 299 | assert.Empty(t, httpProb.OperationID) 300 | 301 | enrichHTTPProblem(httpProb, "delete_resource", NewProblemComponent("test", "v2")) 302 | assert.NotEmpty(t, httpProb.Component) 303 | assert.Equal(t, "test", httpProb.Component.Name) 304 | assert.Equal(t, "v2", httpProb.Component.Version) 305 | assert.Equal(t, "delete_resource", httpProb.OperationID) 306 | } 307 | 308 | func TestPrivateEnrichHTTPProblemWithPopulatedProblem(t *testing.T) { 309 | httpProb := httpErrorf("Bad request", &DetailedResponse{}) 310 | httpProb.Component = NewProblemComponent("some-api", "v3") 311 | httpProb.OperationID = "get_resource" 312 | assert.NotEmpty(t, httpProb.Component) 313 | assert.NotEmpty(t, httpProb.OperationID) 314 | 315 | enrichHTTPProblem(httpProb, "delete_resource", NewProblemComponent("test", "v2")) 316 | assert.Equal(t, "some-api", httpProb.Component.Name) 317 | assert.Equal(t, "v3", httpProb.Component.Version) 318 | assert.Equal(t, "get_resource", httpProb.OperationID) 319 | } 320 | 321 | func getPopulatedHTTPProblem() *HTTPProblem { 322 | return &HTTPProblem{ 323 | IBMProblem: &IBMProblem{ 324 | Summary: "Bad request", 325 | Component: NewProblemComponent("my-service", "v1"), 326 | Severity: ErrorSeverity, 327 | discriminator: "some-issue", 328 | }, 329 | OperationID: "create_resource", 330 | Response: getPopulatedDetailedResponse(), 331 | } 332 | } 333 | 334 | func getPopulatedHTTPProblemWithoutOptionals() *HTTPProblem { 335 | return &HTTPProblem{ 336 | IBMProblem: &IBMProblem{ 337 | Summary: "Bad request", 338 | Component: NewProblemComponent("my-service", "v1"), 339 | Severity: ErrorSeverity, 340 | discriminator: "some-issue", 341 | }, 342 | OperationID: "create_resource", 343 | Response: &DetailedResponse{ 344 | StatusCode: 400, 345 | }, 346 | } 347 | } 348 | 349 | func getPopulatedDetailedResponse() *DetailedResponse { 350 | headers := http.Header{} 351 | headers.Add("Content-Type", "application/json") 352 | headers.Add("X-Request-ID", "abc123") 353 | headers.Add("X-Correlation-ID", "xyz789") 354 | 355 | return &DetailedResponse{ 356 | StatusCode: 400, 357 | Headers: headers, 358 | Result: map[string]interface{}{ 359 | "errorCode": "invalid-input", 360 | }, 361 | } 362 | } 363 | -------------------------------------------------------------------------------- /core/ibm_problem.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | // (C) Copyright IBM Corp. 2024. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | import ( 18 | "errors" 19 | "fmt" 20 | ) 21 | 22 | // problemSeverity simulates an enum by defining the only string values that are 23 | // supported for the Severity field of an IBMProblem: "error" and "warning". 24 | type problemSeverity string 25 | 26 | const ( 27 | ErrorSeverity problemSeverity = "error" 28 | WarningSeverity problemSeverity = "warning" 29 | ) 30 | 31 | // ProblemComponent is a structure that holds information about a given component. 32 | type ProblemComponent struct { 33 | Name string 34 | Version string 35 | } 36 | 37 | func NewProblemComponent(name, version string) *ProblemComponent { 38 | return &ProblemComponent{ 39 | Name: name, 40 | Version: version, 41 | } 42 | } 43 | 44 | // IBMProblem holds the base set of fields that all problem types 45 | // should include. It is geared more towards embedding in other 46 | // structs than towards use on its own. 47 | type IBMProblem struct { 48 | 49 | // Summary is the informative, user-friendly message that describes 50 | // the problem and what caused it. 51 | Summary string 52 | 53 | // Component is a structure providing information about the actual 54 | // component that the problem occurred in: the name of the component 55 | // and the version of the component being used with the problem occurred. 56 | // Examples of components include cloud services, SDK clients, the IBM 57 | // Terraform Provider, etc. For programming libraries, the Component name 58 | // should match the module name for the library (i.e. the name a user 59 | // would use to install it). 60 | Component *ProblemComponent 61 | 62 | // Severity represents the severity level of the problem, 63 | // e.g. error, warning, or info. 64 | Severity problemSeverity 65 | 66 | // discriminator is a private property that is not ever meant to be 67 | // seen by the end user. It's sole purpose is to enforce uniqueness 68 | // for the computed ID of problems that would otherwise have the same 69 | // ID. For example, if two SDKProblem instances are created with the 70 | // same Component and Function values, they would end up with the same 71 | // ID. This property allows us to "discriminate" between such problems. 72 | discriminator string 73 | 74 | // causedBy allows for the storage of a problem from a previous component, 75 | // if there is one. 76 | causedBy Problem 77 | 78 | // nativeCausedBy allows for the storage of an error that is the cause of 79 | // the problem instance but is not a part of the official chain of problem 80 | // types. By including these errors in the "Unwrap" chain, the problem type 81 | // changes become compatible with downstream code that uses error checking 82 | // methods like "Is" and "As". 83 | nativeCausedBy error 84 | } 85 | 86 | // Error returns the problem's message and implements the native 87 | // "error" interface. 88 | func (e *IBMProblem) Error() string { 89 | return e.Summary 90 | } 91 | 92 | // GetBaseSignature provides a convenient way of 93 | // retrieving the fields needed to compute the 94 | // hash that are common to every kind of problem. 95 | func (e *IBMProblem) GetBaseSignature() string { 96 | causedByID := "" 97 | if e.causedBy != nil { 98 | causedByID = e.causedBy.GetID() 99 | } 100 | return fmt.Sprintf("%s%s%s%s", e.Component.Name, e.Severity, e.discriminator, causedByID) 101 | } 102 | 103 | // GetCausedBy returns the underlying "causedBy" problem, if it exists. 104 | func (e *IBMProblem) GetCausedBy() Problem { 105 | return e.causedBy 106 | } 107 | 108 | // Unwrap implements an interface the native Go "errors" package uses to 109 | // check for embedded problems in a given problem instance. IBM problem types 110 | // are not embedded in the traditional sense, but they chain previous 111 | // problem instances together with the "causedBy" field. This allows error 112 | // interface instances to be cast into any of the problem types in the chain 113 | // using the native "errors.As" function. This can be useful for, as an 114 | // example, extracting an HTTPProblem from the chain if it exists. 115 | // Note that this Unwrap method returns only the chain of "caused by" problems; 116 | // it does not include the error instance the method is called on - that is 117 | // looked at separately by the "errors" package in functions like "As". 118 | func (e *IBMProblem) Unwrap() []error { 119 | var errs []error 120 | 121 | // Include native (i.e. non-Problem) caused by errors in the 122 | // chain for compatibility with respect to downstream methods 123 | // like "errors.Is" or "errors.As". 124 | if e.nativeCausedBy != nil { 125 | errs = append(errs, e.nativeCausedBy) 126 | } 127 | 128 | causedBy := e.GetCausedBy() 129 | if causedBy == nil { 130 | return errs 131 | } 132 | 133 | errs = append(errs, causedBy) 134 | 135 | var toUnwrap interface{ Unwrap() []error } 136 | if errors.As(causedBy, &toUnwrap) { 137 | causedByChain := toUnwrap.Unwrap() 138 | if causedByChain != nil { 139 | errs = append(errs, causedByChain...) 140 | } 141 | } 142 | 143 | return errs 144 | } 145 | 146 | func ibmProblemf(err error, severity problemSeverity, component *ProblemComponent, summary, discriminator string) *IBMProblem { 147 | // Leaving summary blank is a convenient way to 148 | // use the message from the underlying problem. 149 | if summary == "" && err != nil { 150 | summary = err.Error() 151 | } 152 | 153 | newError := &IBMProblem{ 154 | Summary: summary, 155 | Component: component, 156 | discriminator: discriminator, 157 | Severity: severity, 158 | } 159 | 160 | var causedBy Problem 161 | if errors.As(err, &causedBy) { 162 | newError.causedBy = causedBy 163 | } else { 164 | newError.nativeCausedBy = err 165 | } 166 | 167 | return newError 168 | } 169 | 170 | // IBMErrorf creates and returns a new instance of an IBMProblem struct with "error" 171 | // level severity. It is primarily meant for embedding IBMProblem structs in other types. 172 | func IBMErrorf(err error, component *ProblemComponent, summary, discriminator string) *IBMProblem { 173 | return ibmProblemf(err, ErrorSeverity, component, summary, discriminator) 174 | } 175 | -------------------------------------------------------------------------------- /core/ibm_problem_test.go: -------------------------------------------------------------------------------- 1 | //go:build all || fast || problem 2 | 3 | package core 4 | 5 | // (C) Copyright IBM Corp. 2024. 6 | // 7 | // Licensed under the Apache License, Version 2.0 (the "License"); 8 | // you may not use this file except in compliance with the License. 9 | // You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, software 14 | // distributed under the License is distributed on an "AS IS" BASIS, 15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | // See the License for the specific language governing permissions and 17 | // limitations under the License. 18 | 19 | import ( 20 | "errors" 21 | "testing" 22 | 23 | "github.com/stretchr/testify/assert" 24 | ) 25 | 26 | func TestNewProblemComponent(t *testing.T) { 27 | name := "my-sdk" 28 | version := "1.2.3" 29 | component := NewProblemComponent(name, version) 30 | 31 | assert.NotNil(t, component) 32 | assert.Equal(t, name, component.Name) 33 | assert.Equal(t, version, component.Version) 34 | } 35 | 36 | func TestIBMProblemError(t *testing.T) { 37 | message := "Wrong!" 38 | ibmProblem := &IBMProblem{ 39 | Summary: message, 40 | } 41 | 42 | assert.Equal(t, message, ibmProblem.Error()) 43 | } 44 | 45 | func TestIBMProblemGetBaseSignature(t *testing.T) { 46 | ibmProblem := &IBMProblem{ 47 | Summary: "Wrong!", 48 | Component: NewProblemComponent("my-sdk", "1.2.3"), 49 | Severity: ErrorSeverity, 50 | discriminator: "some-issue", 51 | causedBy: mockProblem{}, 52 | } 53 | 54 | assert.Equal(t, "my-sdkerrorsome-issuemock-abc123", ibmProblem.GetBaseSignature()) 55 | } 56 | 57 | func TestIBMProblemGetBaseSignatureNoCausedBy(t *testing.T) { 58 | ibmProblem := &IBMProblem{ 59 | Summary: "Wrong!", 60 | Component: NewProblemComponent("my-sdk", "1.2.3"), 61 | Severity: ErrorSeverity, 62 | discriminator: "some-issue", 63 | } 64 | 65 | assert.Equal(t, "my-sdkerrorsome-issue", ibmProblem.GetBaseSignature()) 66 | } 67 | 68 | func TestIBMProblemGetBaseSignatureNoDiscriminator(t *testing.T) { 69 | ibmProblem := &IBMProblem{ 70 | Summary: "Wrong!", 71 | Component: NewProblemComponent("my-sdk", "1.2.3"), 72 | Severity: ErrorSeverity, 73 | causedBy: mockProblem{}, 74 | } 75 | 76 | assert.Equal(t, "my-sdkerrormock-abc123", ibmProblem.GetBaseSignature()) 77 | } 78 | 79 | func TestIBMProblemGetCausedBy(t *testing.T) { 80 | ibmProblem := &IBMProblem{ 81 | Summary: "Wrong!", 82 | } 83 | 84 | assert.Nil(t, ibmProblem.GetCausedBy()) 85 | 86 | data := "test" 87 | ibmProblem = &IBMProblem{ 88 | causedBy: mockProblem{ 89 | Data: data, 90 | }, 91 | } 92 | 93 | cb := ibmProblem.GetCausedBy() 94 | assert.NotNil(t, cb) 95 | 96 | mock, ok := cb.(mockProblem) 97 | assert.True(t, ok) 98 | assert.Equal(t, data, mock.Data) 99 | } 100 | 101 | // Note: the "Unwrap" method isn't intended to be invoked 102 | // directly, but to enable "errors.As" to populate errors 103 | // with "caused by" problems. So, that's what we test. 104 | func TestIBMProblemUnwrap(t *testing.T) { 105 | data := "test" 106 | 107 | err := &IBMProblem{ 108 | Summary: data, 109 | causedBy: mockProblem{ 110 | Data: data, 111 | }, 112 | } 113 | 114 | assert.Equal(t, data, err.Error()) 115 | 116 | var ibmProb *IBMProblem 117 | isIBMProb := errors.As(err, &ibmProb) 118 | assert.True(t, isIBMProb) 119 | assert.Equal(t, data, ibmProb.Summary) 120 | 121 | var mock mockProblem 122 | ismockProblem := errors.As(err, &mock) 123 | assert.True(t, ismockProblem) 124 | assert.Equal(t, data, mock.Data) 125 | } 126 | 127 | func TestIBMProblemf(t *testing.T) { 128 | data := "data" 129 | causedBy := mockProblem{Data: data} 130 | severity := WarningSeverity 131 | componentName := "my-sdk" 132 | componentVersion := "1.2.3" 133 | component := NewProblemComponent(componentName, componentVersion) 134 | summary := "Wrong!" 135 | discriminator := "some-issue" 136 | 137 | ibmProblem := ibmProblemf(causedBy, severity, component, summary, discriminator) 138 | assert.NotNil(t, ibmProblem) 139 | assert.Equal(t, causedBy, ibmProblem.causedBy) 140 | assert.Equal(t, severity, ibmProblem.Severity) 141 | assert.Equal(t, component, ibmProblem.Component) 142 | assert.Equal(t, summary, ibmProblem.Summary) 143 | assert.Equal(t, discriminator, ibmProblem.discriminator) 144 | } 145 | 146 | func TestIBMProblemfNoCausedBy(t *testing.T) { 147 | severity := ErrorSeverity 148 | componentName := "my-sdk" 149 | componentVersion := "1.2.3" 150 | component := NewProblemComponent(componentName, componentVersion) 151 | summary := "Wrong!" 152 | discriminator := "some-issue" 153 | 154 | ibmProblem := ibmProblemf(nil, severity, component, summary, discriminator) 155 | assert.NotNil(t, ibmProblem) 156 | assert.Nil(t, ibmProblem.causedBy) 157 | assert.Equal(t, severity, ibmProblem.Severity) 158 | assert.Equal(t, component, ibmProblem.Component) 159 | assert.Equal(t, summary, ibmProblem.Summary) 160 | assert.Equal(t, discriminator, ibmProblem.discriminator) 161 | } 162 | 163 | func TestIBMProblemfCausedByNotProblem(t *testing.T) { 164 | severity := WarningSeverity 165 | componentName := "my-sdk" 166 | componentVersion := "1.2.3" 167 | component := NewProblemComponent(componentName, componentVersion) 168 | summary := "Wrong!" 169 | discriminator := "some-issue" 170 | 171 | ibmProblem := ibmProblemf(errors.New("unused"), severity, component, summary, discriminator) 172 | assert.NotNil(t, ibmProblem) 173 | assert.Nil(t, ibmProblem.causedBy) 174 | assert.Equal(t, severity, ibmProblem.Severity) 175 | assert.Equal(t, component, ibmProblem.Component) 176 | assert.Equal(t, summary, ibmProblem.Summary) 177 | assert.Equal(t, discriminator, ibmProblem.discriminator) 178 | } 179 | 180 | func TestIBMProblemfNoSummary(t *testing.T) { 181 | data := "data" 182 | causedBy := mockProblem{Data: data} 183 | severity := WarningSeverity 184 | componentName := "my-sdk" 185 | componentVersion := "1.2.3" 186 | component := NewProblemComponent(componentName, componentVersion) 187 | discriminator := "some-issue" 188 | 189 | ibmProblem := ibmProblemf(causedBy, severity, component, "", discriminator) 190 | assert.NotNil(t, ibmProblem) 191 | assert.Equal(t, causedBy, ibmProblem.causedBy) 192 | assert.Equal(t, severity, ibmProblem.Severity) 193 | assert.Equal(t, component, ibmProblem.Component) 194 | assert.Equal(t, data, ibmProblem.Summary) 195 | assert.Equal(t, discriminator, ibmProblem.discriminator) 196 | } 197 | 198 | func TestIBMErrorf(t *testing.T) { 199 | data := "data" 200 | causedBy := mockProblem{Data: data} 201 | componentName := "my-sdk" 202 | componentVersion := "1.2.3" 203 | component := NewProblemComponent(componentName, componentVersion) 204 | summary := "Wrong!" 205 | discriminator := "some-issue" 206 | 207 | ibmProblem := IBMErrorf(causedBy, component, summary, discriminator) 208 | assert.NotNil(t, ibmProblem) 209 | assert.Equal(t, causedBy, ibmProblem.causedBy) 210 | assert.Equal(t, ErrorSeverity, ibmProblem.Severity) 211 | assert.Equal(t, component, ibmProblem.Component) 212 | assert.Equal(t, summary, ibmProblem.Summary) 213 | assert.Equal(t, discriminator, ibmProblem.discriminator) 214 | } 215 | 216 | func TestIBMErrorfNoCausedBy(t *testing.T) { 217 | componentName := "my-sdk" 218 | componentVersion := "1.2.3" 219 | component := NewProblemComponent(componentName, componentVersion) 220 | summary := "Wrong!" 221 | discriminator := "some-issue" 222 | 223 | ibmProblem := IBMErrorf(nil, component, summary, discriminator) 224 | assert.NotNil(t, ibmProblem) 225 | assert.Nil(t, ibmProblem.causedBy) 226 | assert.Equal(t, ErrorSeverity, ibmProblem.Severity) 227 | assert.Equal(t, component, ibmProblem.Component) 228 | assert.Equal(t, summary, ibmProblem.Summary) 229 | assert.Equal(t, discriminator, ibmProblem.discriminator) 230 | } 231 | 232 | func TestIBMErrorfCausedByNotProblem(t *testing.T) { 233 | componentName := "my-sdk" 234 | componentVersion := "1.2.3" 235 | component := NewProblemComponent(componentName, componentVersion) 236 | summary := "Wrong!" 237 | discriminator := "some-issue" 238 | 239 | ibmProblem := IBMErrorf(errors.New("unused"), component, summary, discriminator) 240 | assert.NotNil(t, ibmProblem) 241 | assert.Nil(t, ibmProblem.causedBy) 242 | assert.Equal(t, ErrorSeverity, ibmProblem.Severity) 243 | assert.Equal(t, component, ibmProblem.Component) 244 | assert.Equal(t, summary, ibmProblem.Summary) 245 | assert.Equal(t, discriminator, ibmProblem.discriminator) 246 | } 247 | 248 | func TestIBMErrorfNoSummary(t *testing.T) { 249 | data := "data" 250 | causedBy := mockProblem{Data: data} 251 | componentName := "my-sdk" 252 | componentVersion := "1.2.3" 253 | component := NewProblemComponent(componentName, componentVersion) 254 | discriminator := "some-issue" 255 | 256 | ibmProblem := IBMErrorf(causedBy, component, "", discriminator) 257 | assert.NotNil(t, ibmProblem) 258 | assert.Equal(t, causedBy, ibmProblem.causedBy) 259 | assert.Equal(t, ErrorSeverity, ibmProblem.Severity) 260 | assert.Equal(t, component, ibmProblem.Component) 261 | assert.Equal(t, data, ibmProblem.Summary) 262 | assert.Equal(t, discriminator, ibmProblem.discriminator) 263 | } 264 | 265 | func TestProblemSeverityConstants(t *testing.T) { 266 | // The values should be equal but the types should not be. 267 | assert.NotEqual(t, "error", ErrorSeverity) 268 | assert.EqualValues(t, "error", ErrorSeverity) 269 | 270 | assert.NotEqual(t, "warning", WarningSeverity) 271 | assert.EqualValues(t, "warning", WarningSeverity) 272 | } 273 | 274 | type mockProblem struct { 275 | Data string 276 | } 277 | 278 | func (m mockProblem) GetConsoleMessage() string { 279 | return "" 280 | } 281 | func (m mockProblem) GetDebugMessage() string { 282 | return "" 283 | } 284 | func (m mockProblem) GetID() string { 285 | return "mock-abc123" 286 | } 287 | func (m mockProblem) Error() string { 288 | return m.Data 289 | } 290 | func (m mockProblem) GetConsoleOrderedMaps() *OrderedMaps { 291 | orderedMaps := NewOrderedMaps() 292 | orderedMaps.Add("id", m.GetID()) 293 | return orderedMaps 294 | } 295 | func (m mockProblem) GetDebugOrderedMaps() *OrderedMaps { 296 | orderedMaps := m.GetConsoleOrderedMaps() 297 | orderedMaps.Add("data", m.Data) 298 | return orderedMaps 299 | } 300 | -------------------------------------------------------------------------------- /core/jwt_utils.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | // (C) Copyright IBM Corp. 2021. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | import ( 18 | "encoding/base64" 19 | "encoding/json" 20 | "errors" 21 | "fmt" 22 | "strings" 23 | ) 24 | 25 | // coreJWTClaims are the fields within a JWT's "claims" segment that we're interested in. 26 | type coreJWTClaims struct { 27 | ExpiresAt int64 `json:"exp,omitempty"` 28 | IssuedAt int64 `json:"iat,omitempty"` 29 | } 30 | 31 | // parseJWT parses the specified JWT token string and returns an instance of the coreJWTClaims struct. 32 | func parseJWT(tokenString string) (claims *coreJWTClaims, err error) { 33 | // A JWT consists of three .-separated segments 34 | segments := strings.Split(tokenString, ".") 35 | if len(segments) != 3 { 36 | err = errors.New("token contains an invalid number of segments") 37 | err = SDKErrorf(err, "", "need-3-segs", getComponentInfo()) 38 | return 39 | } 40 | 41 | // Parse Claims segment. 42 | var claimBytes []byte 43 | claimBytes, err = decodeSegment(segments[1]) 44 | if err != nil { 45 | return 46 | } 47 | 48 | // Now deserialize the claims segment into our coreClaims struct. 49 | claims = &coreJWTClaims{} 50 | err = json.Unmarshal(claimBytes, claims) 51 | if err != nil { 52 | err = fmt.Errorf("error unmarshalling token: %s", err.Error()) 53 | err = SDKErrorf(err, "", "bad-token", getComponentInfo()) 54 | return 55 | } 56 | 57 | return 58 | } 59 | 60 | // Decode JWT specific base64url encoding with padding stripped 61 | // Copied from https://github.com/golang-jwt/jwt/blob/main/token.go 62 | func decodeSegment(seg string) ([]byte, error) { 63 | if l := len(seg) % 4; l > 0 { 64 | seg += strings.Repeat("=", 4-l) 65 | } 66 | 67 | res, err := base64.URLEncoding.DecodeString(seg) 68 | if err != nil { 69 | err = SDKErrorf(err, fmt.Sprintf("error decoding claims segment: %s", err.Error()), "bad-claim-seg", getComponentInfo()) 70 | } 71 | return res, err 72 | } 73 | -------------------------------------------------------------------------------- /core/jwt_utils_test.go: -------------------------------------------------------------------------------- 1 | //go:build all || fast || auth 2 | 3 | package core 4 | 5 | // (C) Copyright IBM Corp. 2021. 6 | // 7 | // Licensed under the Apache License, Version 2.0 (the "License"); 8 | // you may not use this file except in compliance with the License. 9 | // You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, software 14 | // distributed under the License is distributed on an "AS IS" BASIS, 15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | // See the License for the specific language governing permissions and 17 | // limitations under the License. 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/stretchr/testify/assert" 23 | ) 24 | 25 | const ( 26 | // These two access tokens are the result of running curl to invoke 27 | // the POST /v1/authorize against a CP4D environment. 28 | 29 | // Username/password 30 | // curl -k -X POST https:///icp4d-api/v1/authorize -H 'Content-Type: application/json' \ 31 | // -d '{"username": "testuser", "password": "" }' 32 | jwtUserPwd = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InRlc3R1c2VyIiwicm9sZSI6IlVzZXIiLCJwZXJtaXNzaW9ucyI6WyJhY2Nlc3NfY2F0YWxvZyIsImNhbl9wcm92aXNpb24iLCJzaWduX2luX29ubHkiXSwiZ3JvdXBzIjpbMTAwMDBdLCJzdWIiOiJ0ZXN0dXNlciIsImlzcyI6IktOT1hTU08iLCJhdWQiOiJEU1giLCJ1aWQiOiIxMDAwMzMxMDAzIiwiYXV0aGVudGljYXRvciI6ImRlZmF1bHQiLCJpYXQiOjE2MTA1NDgxNjksImV4cCI6MTYxMDU5MTMzM30.AGbjQwWDQ7KG7Ef5orTH982kLmwExmj0eiDe3nke8frcm0EfshglU1nddIVBhrEI6vkrHZQSUoolLT6Kz1hUrbbRedC6E-XmJwPG9HcfG9BsW6CJ4hN5IbrJDf9maDBvKDLsEjH6YPTiAoMDNKsxLImHFms0GbIREAj_7Q7Xb2jpQYPR1JG32GAclq01deY8n4whE6WeyQqcbHUCGy3Q7sKddqEvT59XjLr1Mwm1uvIGnso_FkWJhvZs_z4aF0rVQes7gJZpOOSPkuA7l08KxvFmX3vF0IqmfudymEqaW9YH2ihAvHQBOJJtIkKaRga2TYyvfcwLFCXOABEi2lBOuQ" // #nosec 33 | 34 | // Username/apikey 35 | // curl -k -X POST https:///icp4d-api/v1/authorize -H 'Content-Type: application/json' \ 36 | // -d '{"username": "testuser", "api_key": "" }' 37 | jwtUserApikey = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InRlc3R1c2VyIiwicm9sZSI6IlVzZXIiLCJwZXJtaXNzaW9ucyI6WyJhY2Nlc3NfY2F0YWxvZyIsImNhbl9wcm92aXNpb24iLCJzaWduX2luX29ubHkiXSwiZ3JvdXBzIjpbMTAwMDBdLCJzdWIiOiJ0ZXN0dXNlciIsImlzcyI6IktOT1hTU08iLCJhdWQiOiJEU1giLCJ1aWQiOiIxMDAwMzMxMDAzIiwiYXV0aGVudGljYXRvciI6ImRlZmF1bHQiLCJpYXQiOjE2MTA1NDgyNDgsImV4cCI6MTYxMDU5MTQxMn0.I8MgxrapKRt0nOn0F41NtLHQ5HGmInZNaJIWcNwyBgLWI5YY_98kpKLecN5d9Ll9g0_lapAFs_b8xpTya0Lvnp2Q81SloRFpDhAMUVHVWq46g2dvZd1JpoFB8NHwrkz2qE_JUHBIonJmQusy8vMm1m1CPy0pE6fTYH1d5EJG2vLo6f2eFiDizLfGxb0ym9lUOkK6dgNZw2T32N8IoSYNan6BQU25Jai6llWRLwZda7R521EPEw2AtPDsd95AxoTd8f4pptxfkL2uXpT35wRguap_09sRlvDTR18Ghs-GbtCh3Do-8OPGEFYKvJkSHNpiXPw8pvHEe5jCGl3l3F5vXQ" // #nosec 38 | ) 39 | 40 | func TestParseJWT(t *testing.T) { 41 | var err error 42 | var claims *coreJWTClaims 43 | 44 | claims, err = parseJWT(jwtUserPwd) 45 | assert.Nil(t, err) 46 | assert.NotNil(t, claims) 47 | assert.Equal(t, int64(1610591333), claims.ExpiresAt) 48 | assert.Equal(t, int64(1610548169), claims.IssuedAt) 49 | 50 | claims, err = parseJWT(jwtUserApikey) 51 | assert.Nil(t, err) 52 | assert.NotNil(t, claims) 53 | assert.Equal(t, int64(1610591412), claims.ExpiresAt) 54 | assert.Equal(t, int64(1610548248), claims.IssuedAt) 55 | } 56 | 57 | func TestParseJWTFail(t *testing.T) { 58 | _, err := parseJWT("segment1.segment2") 59 | assert.NotNil(t, err) 60 | t.Logf("Expected error: %s\n", err.Error()) 61 | 62 | _, err = parseJWT("====.====.====") 63 | assert.NotNil(t, err) 64 | t.Logf("Expected error: %s\n", err.Error()) 65 | 66 | _, err = parseJWT("segment1.segment2.segment3") 67 | assert.NotNil(t, err) 68 | t.Logf("Expected error: %s\n", err.Error()) 69 | } 70 | 71 | func TestDecodeSegment(t *testing.T) { 72 | testStringDecoded := "testString\n" 73 | testStringEncoded := "dGVzdFN0cmluZwo=" 74 | testStringEncodedShort := "dGVzdFN0cmluZwo" 75 | testStringInvalid := "???!" 76 | 77 | var err error 78 | var decoded []byte 79 | 80 | decoded, err = decodeSegment(testStringEncoded) 81 | assert.Nil(t, err) 82 | assert.Equal(t, testStringDecoded, string(decoded)) 83 | 84 | decoded, err = decodeSegment(testStringEncodedShort) 85 | assert.Nil(t, err) 86 | assert.Equal(t, testStringDecoded, string(decoded)) 87 | 88 | decoded, err = decodeSegment("") 89 | assert.Nil(t, err) 90 | assert.Equal(t, []byte{}, decoded) 91 | 92 | _, err = decodeSegment(testStringInvalid) 93 | assert.NotNil(t, err) 94 | } 95 | -------------------------------------------------------------------------------- /core/log.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | // (C) Copyright IBM Corp. 2020, 2021. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | import ( 18 | "log" 19 | "os" 20 | "sync" 21 | ) 22 | 23 | // LogLevel defines a type for logging levels 24 | type LogLevel int 25 | 26 | // Log level constants 27 | const ( 28 | LevelNone LogLevel = iota 29 | LevelError 30 | LevelWarn 31 | LevelInfo 32 | LevelDebug 33 | ) 34 | 35 | // Logger is the logging interface implemented and used by the Go core library. 36 | // Users of the library can supply their own implementation by calling SetLogger(). 37 | type Logger interface { 38 | Log(level LogLevel, format string, inserts ...interface{}) 39 | Error(format string, inserts ...interface{}) 40 | Warn(format string, inserts ...interface{}) 41 | Info(format string, inserts ...interface{}) 42 | Debug(format string, inserts ...interface{}) 43 | 44 | SetLogLevel(level LogLevel) 45 | GetLogLevel() LogLevel 46 | IsLogLevelEnabled(level LogLevel) bool 47 | } 48 | 49 | // SDKLoggerImpl is the Go core's implementation of the Logger interface. 50 | // This logger contains two instances of Go's log.Logger interface which are 51 | // used to perform message logging. 52 | // "infoLogger" is used to log info/warn/debug messages. 53 | // If specified as nil, then a default log.Logger instance that uses stdout will be created 54 | // and used for "infoLogger". 55 | // "errorLogger" is used to log error messages. 56 | // If specified as nil, then a default log.Logger instance that uses stderr will be created 57 | // and used for "errorLogger". 58 | type SDKLoggerImpl struct { 59 | 60 | // The current log level configured in this logger. 61 | // Only messages with a log level that is <= 'logLevel' will be displayed. 62 | logLevel LogLevel 63 | 64 | // The underlying log.Logger instances used to log info/warn/debug messages. 65 | infoLogger *log.Logger 66 | 67 | // The underlying log.Logger instances used to log error messages. 68 | errorLogger *log.Logger 69 | 70 | // These are used to initialize the loggers above. 71 | infoInit sync.Once 72 | errorInit sync.Once 73 | } 74 | 75 | // SetLogLevel sets level to be the current logging level 76 | func (l *SDKLoggerImpl) SetLogLevel(level LogLevel) { 77 | l.logLevel = level 78 | } 79 | 80 | // GetLogLevel sets level to be the current logging level 81 | func (l *SDKLoggerImpl) GetLogLevel() LogLevel { 82 | return l.logLevel 83 | } 84 | 85 | // IsLogLevelEnabled returns true iff the logger's current logging level 86 | // indicates that 'level' is enabled. 87 | func (l *SDKLoggerImpl) IsLogLevelEnabled(level LogLevel) bool { 88 | return l.logLevel >= level 89 | } 90 | 91 | // infoLog returns the underlying log.Logger instance used for info/warn/debug logging. 92 | func (l *SDKLoggerImpl) infoLog() *log.Logger { 93 | l.infoInit.Do(func() { 94 | if l.infoLogger == nil { 95 | l.infoLogger = log.New(os.Stdout, "", log.LstdFlags) 96 | } 97 | }) 98 | 99 | return l.infoLogger 100 | } 101 | 102 | // errorLog returns the underlying log.Logger instance used for error logging. 103 | func (l *SDKLoggerImpl) errorLog() *log.Logger { 104 | l.errorInit.Do(func() { 105 | if l.errorLogger == nil { 106 | l.errorLogger = log.New(os.Stderr, "", log.LstdFlags) 107 | } 108 | }) 109 | 110 | return l.errorLogger 111 | } 112 | 113 | // Log will log the specified message on the appropriate log.Logger instance if "level" is currently enabled. 114 | func (l *SDKLoggerImpl) Log(level LogLevel, format string, inserts ...interface{}) { 115 | if l.IsLogLevelEnabled(level) { 116 | var goLogger *log.Logger 117 | switch level { 118 | case LevelError: 119 | goLogger = l.errorLog() 120 | default: 121 | goLogger = l.infoLog() 122 | } 123 | goLogger.Printf(format, inserts...) 124 | } 125 | } 126 | 127 | // Error logs a message at level "Error" 128 | func (l *SDKLoggerImpl) Error(format string, inserts ...interface{}) { 129 | l.Log(LevelError, "[Error] "+format, inserts...) 130 | } 131 | 132 | // Warn logs a message at level "Warn" 133 | func (l *SDKLoggerImpl) Warn(format string, inserts ...interface{}) { 134 | l.Log(LevelWarn, "[Warn] "+format, inserts...) 135 | } 136 | 137 | // Info logs a message at level "Info" 138 | func (l *SDKLoggerImpl) Info(format string, inserts ...interface{}) { 139 | l.Log(LevelInfo, "[Info] "+format, inserts...) 140 | } 141 | 142 | // Debug logs a message at level "Debug" 143 | func (l *SDKLoggerImpl) Debug(format string, inserts ...interface{}) { 144 | l.Log(LevelDebug, "[Debug] "+format, inserts...) 145 | } 146 | 147 | // NewLogger constructs an SDKLoggerImpl instance with the specified logging level 148 | // enabled. 149 | // The "infoLogger" parameter is the log.Logger instance to be used to log 150 | // info/warn/debug messages. If specified as nil, then a default log.Logger instance 151 | // that writes messages to "stdout" will be used. 152 | // The "errorLogger" parameter is the log.Logger instance to be used to log 153 | // error messages. If specified as nil, then a default log.Logger instance 154 | // that writes messages to "stderr" will be used. 155 | func NewLogger(level LogLevel, infoLogger *log.Logger, errorLogger *log.Logger) *SDKLoggerImpl { 156 | return &SDKLoggerImpl{ 157 | logLevel: level, 158 | infoLogger: infoLogger, 159 | errorLogger: errorLogger, 160 | } 161 | } 162 | 163 | // sdkLogger holds the Logger implementation used by the Go core library. 164 | var sdkLogger Logger = NewLogger(LevelError, nil, nil) 165 | 166 | // SetLogger sets the specified Logger instance as the logger to be used by the Go core library. 167 | func SetLogger(logger Logger) { 168 | sdkLogger = logger 169 | } 170 | 171 | // GetLogger returns the Logger instance currently used by the Go core. 172 | func GetLogger() Logger { 173 | return sdkLogger 174 | } 175 | 176 | // SetLoggingLevel will enable the specified logging level in the Go core library. 177 | func SetLoggingLevel(level LogLevel) { 178 | GetLogger().SetLogLevel(level) 179 | } 180 | -------------------------------------------------------------------------------- /core/log_test.go: -------------------------------------------------------------------------------- 1 | //go:build all || fast || log 2 | 3 | package core 4 | 5 | // (C) Copyright IBM Corp. 2020, 2021. 6 | // 7 | // Licensed under the Apache License, Version 2.0 (the "License"); 8 | // you may not use this file except in compliance with the License. 9 | // You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, software 14 | // distributed under the License is distributed on an "AS IS" BASIS, 15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | // See the License for the specific language governing permissions and 17 | // limitations under the License. 18 | 19 | import ( 20 | "bytes" 21 | "log" 22 | "testing" 23 | 24 | "github.com/stretchr/testify/assert" 25 | ) 26 | 27 | func TestSetLogLevel(t *testing.T) { 28 | l := NewLogger(LevelNone, nil, nil) 29 | assert.NotNil(t, l) 30 | 31 | errorLogger := l.errorLog() 32 | assert.NotNil(t, errorLogger) 33 | 34 | infoLogger := l.infoLog() 35 | assert.NotNil(t, infoLogger) 36 | 37 | l.SetLogLevel(LevelError) 38 | assert.Equal(t, LevelError, l.GetLogLevel()) 39 | assert.True(t, l.IsLogLevelEnabled(LevelError)) 40 | assert.False(t, l.IsLogLevelEnabled(LevelWarn)) 41 | assert.False(t, l.IsLogLevelEnabled(LevelInfo)) 42 | assert.False(t, l.IsLogLevelEnabled(LevelDebug)) 43 | 44 | l.SetLogLevel(LevelWarn) 45 | assert.Equal(t, LevelWarn, l.GetLogLevel()) 46 | assert.True(t, l.IsLogLevelEnabled(LevelError)) 47 | assert.True(t, l.IsLogLevelEnabled(LevelWarn)) 48 | assert.False(t, l.IsLogLevelEnabled(LevelInfo)) 49 | assert.False(t, l.IsLogLevelEnabled(LevelDebug)) 50 | 51 | l.SetLogLevel(LevelInfo) 52 | assert.Equal(t, LevelInfo, l.GetLogLevel()) 53 | assert.True(t, l.IsLogLevelEnabled(LevelError)) 54 | assert.True(t, l.IsLogLevelEnabled(LevelWarn)) 55 | assert.True(t, l.IsLogLevelEnabled(LevelInfo)) 56 | assert.False(t, l.IsLogLevelEnabled(LevelDebug)) 57 | 58 | l.SetLogLevel(LevelDebug) 59 | assert.Equal(t, LevelDebug, l.GetLogLevel()) 60 | assert.True(t, l.IsLogLevelEnabled(LevelError)) 61 | assert.True(t, l.IsLogLevelEnabled(LevelWarn)) 62 | assert.True(t, l.IsLogLevelEnabled(LevelInfo)) 63 | assert.True(t, l.IsLogLevelEnabled(LevelDebug)) 64 | } 65 | 66 | func TestSetLoggingLevel(t *testing.T) { 67 | l := NewLogger(LevelNone, nil, nil) 68 | assert.NotNil(t, l) 69 | 70 | SetLogger(l) 71 | 72 | SetLoggingLevel(LevelError) 73 | assert.Equal(t, LevelError, GetLogger().GetLogLevel()) 74 | 75 | SetLoggingLevel(LevelWarn) 76 | assert.Equal(t, LevelWarn, GetLogger().GetLogLevel()) 77 | 78 | SetLoggingLevel(LevelInfo) 79 | assert.Equal(t, LevelInfo, GetLogger().GetLogLevel()) 80 | 81 | SetLoggingLevel(LevelDebug) 82 | assert.Equal(t, LevelDebug, GetLogger().GetLogLevel()) 83 | 84 | } 85 | 86 | func stringLogger(level LogLevel) (stdout *bytes.Buffer, stderr *bytes.Buffer, logger Logger) { 87 | stdout = new(bytes.Buffer) 88 | stderr = new(bytes.Buffer) 89 | 90 | logger = &SDKLoggerImpl{ 91 | logLevel: level, 92 | infoLogger: log.New(stdout, "", 0), 93 | errorLogger: log.New(stderr, "", 0), 94 | } 95 | return 96 | } 97 | 98 | func TestLogNone(t *testing.T) { 99 | stdout, stderr, l := stringLogger(LevelNone) 100 | 101 | l.Error("error msg") 102 | assert.Empty(t, stdout.String()) 103 | assert.Empty(t, stderr.String()) 104 | 105 | l.Warn("warn msg") 106 | assert.Empty(t, stdout.String()) 107 | assert.Empty(t, stderr.String()) 108 | 109 | l.Info("info msg") 110 | assert.Empty(t, stdout.String()) 111 | assert.Empty(t, stderr.String()) 112 | 113 | l.Debug("debug msg") 114 | assert.Empty(t, stdout.String()) 115 | assert.Empty(t, stderr.String()) 116 | } 117 | 118 | func TestLogError(t *testing.T) { 119 | stdout, stderr, l := stringLogger(LevelError) 120 | 121 | l.Error("error msg") 122 | assert.Empty(t, stdout.String()) 123 | assert.Equal(t, "[Error] error msg\n", stderr.String()) 124 | 125 | stdout.Reset() 126 | stderr.Reset() 127 | l.Warn("warn msg") 128 | assert.Empty(t, stdout.String()) 129 | assert.Empty(t, stderr.String()) 130 | 131 | stdout.Reset() 132 | stderr.Reset() 133 | l.Info("info msg") 134 | assert.Empty(t, stdout.String()) 135 | assert.Empty(t, stderr.String()) 136 | 137 | stdout.Reset() 138 | stderr.Reset() 139 | l.Debug("debug msg") 140 | assert.Empty(t, stdout.String()) 141 | assert.Empty(t, stderr.String()) 142 | } 143 | 144 | func TestLogWarn(t *testing.T) { 145 | stdout, stderr, l := stringLogger(LevelWarn) 146 | 147 | l.Error("error msg") 148 | assert.Empty(t, stdout.String()) 149 | assert.Equal(t, "[Error] error msg\n", stderr.String()) 150 | 151 | stdout.Reset() 152 | stderr.Reset() 153 | l.Warn("warn msg") 154 | assert.Equal(t, "[Warn] warn msg\n", stdout.String()) 155 | assert.Empty(t, stderr.String()) 156 | 157 | stdout.Reset() 158 | stderr.Reset() 159 | l.Info("info msg") 160 | assert.Empty(t, stdout.String()) 161 | assert.Empty(t, stderr.String()) 162 | 163 | stdout.Reset() 164 | stderr.Reset() 165 | l.Debug("debug msg") 166 | assert.Empty(t, stdout.String()) 167 | assert.Empty(t, stderr.String()) 168 | } 169 | 170 | func TestLogInfo(t *testing.T) { 171 | stdout, stderr, l := stringLogger(LevelInfo) 172 | 173 | l.Error("error msg") 174 | assert.Empty(t, stdout.String()) 175 | assert.Equal(t, "[Error] error msg\n", stderr.String()) 176 | 177 | stdout.Reset() 178 | stderr.Reset() 179 | l.Warn("warn msg") 180 | assert.Equal(t, "[Warn] warn msg\n", stdout.String()) 181 | assert.Empty(t, stderr.String()) 182 | 183 | stdout.Reset() 184 | stderr.Reset() 185 | l.Info("info msg") 186 | assert.Equal(t, "[Info] info msg\n", stdout.String()) 187 | assert.Empty(t, stderr.String()) 188 | 189 | stdout.Reset() 190 | stderr.Reset() 191 | l.Debug("debug msg") 192 | assert.Empty(t, stdout.String()) 193 | assert.Empty(t, stderr.String()) 194 | } 195 | 196 | func TestLogDebug(t *testing.T) { 197 | stdout, stderr, l := stringLogger(LevelDebug) 198 | 199 | l.Error("error msg") 200 | assert.Empty(t, stdout.String()) 201 | assert.Equal(t, "[Error] error msg\n", stderr.String()) 202 | 203 | stdout.Reset() 204 | stderr.Reset() 205 | l.Warn("warn msg") 206 | assert.Equal(t, "[Warn] warn msg\n", stdout.String()) 207 | assert.Empty(t, stderr.String()) 208 | 209 | stdout.Reset() 210 | stderr.Reset() 211 | l.Info("info msg") 212 | assert.Equal(t, "[Info] info msg\n", stdout.String()) 213 | assert.Empty(t, stderr.String()) 214 | 215 | stdout.Reset() 216 | stderr.Reset() 217 | l.Debug("debug msg") 218 | assert.Equal(t, "[Debug] debug msg\n", stdout.String()) 219 | assert.Empty(t, stderr.String()) 220 | } 221 | -------------------------------------------------------------------------------- /core/marshal_nulls_test.go: -------------------------------------------------------------------------------- 1 | //go:build all || fast 2 | 3 | package core 4 | 5 | import ( 6 | "bytes" 7 | "encoding/json" 8 | "reflect" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | // 15 | // The purpose of this testcase is to ensure that dynamic properties with nil values are 16 | // correctly serialized as JSON null values. 17 | // In this testcase we have a struct that simulates a generated model with additional properties 18 | // of type string (actuall a *string). 19 | // In addition to the struct, we have methods SetProperty() and GetProperty() which would normally 20 | // be generated for a dynamic model. 21 | // And to round out the simulation, we have methods MarshalJSON() and unmarshalDynamicModel() which 22 | // also simulate methods that are generated for a dynamic model. 23 | // Note that if the SDK generator is modified to change the way in which any of these methods are generated, 24 | // this testcase can be updated to reflect the new generated code and the testcase can continue to 25 | // serve as a test of serialize null dynamic property values. 26 | 27 | type dynamicModel struct { 28 | Prop1 *string `json:"prop1,omitempty"` 29 | Prop2 *int64 `json:"prop2,omitempty"` 30 | additionalProperties map[string]*string 31 | } 32 | 33 | func (o *dynamicModel) SetProperty(key string, value *string) { 34 | if o.additionalProperties == nil { 35 | o.additionalProperties = make(map[string]*string) 36 | } 37 | o.additionalProperties[key] = value 38 | } 39 | 40 | func (o *dynamicModel) GetProperty(key string) *string { 41 | return o.additionalProperties[key] 42 | } 43 | 44 | func (o *dynamicModel) MarshalJSON() (buffer []byte, err error) { 45 | m := make(map[string]interface{}) 46 | if len(o.additionalProperties) > 0 { 47 | for k, v := range o.additionalProperties { 48 | m[k] = v 49 | } 50 | } 51 | if o.Prop1 != nil { 52 | m["prop1"] = o.Prop1 53 | } 54 | if o.Prop2 != nil { 55 | m["prop2"] = o.Prop2 56 | } 57 | buffer, err = json.Marshal(m) 58 | return 59 | } 60 | 61 | func unmarshalDynamicModel(m map[string]json.RawMessage, result interface{}) (err error) { 62 | obj := new(dynamicModel) 63 | err = UnmarshalPrimitive(m, "prop1", &obj.Prop1) 64 | if err != nil { 65 | return 66 | } 67 | delete(m, "prop1") 68 | err = UnmarshalPrimitive(m, "prop2", &obj.Prop2) 69 | if err != nil { 70 | return 71 | } 72 | delete(m, "prop2") 73 | for k := range m { 74 | var v *string 75 | e := UnmarshalPrimitive(m, k, &v) 76 | if e != nil { 77 | err = e 78 | return 79 | } 80 | obj.SetProperty(k, v) 81 | } 82 | reflect.ValueOf(result).Elem().Set(reflect.ValueOf(obj)) 83 | return 84 | } 85 | 86 | func TestAdditionalPropertiesNull(t *testing.T) { 87 | // Construct an instance of the model so that it includes a dynamic property with value nil. 88 | model := &dynamicModel{ 89 | Prop1: StringPtr("foo"), 90 | Prop2: Int64Ptr(38), 91 | } 92 | model.SetProperty("bar", nil) 93 | 94 | // Serialize to JSON and ensure that the nil dynamic property value was explicitly serialized as JSON null. 95 | b, err := json.Marshal(model) 96 | jsonString := string(b) 97 | assert.Nil(t, err) 98 | t.Logf("Serialized model: %s\n", jsonString) 99 | assert.Contains(t, jsonString, `"bar":null`) 100 | 101 | // Next, deserialize the json string into a map of RawMessages to simulate how the SDK code will 102 | // deserialize a response body. 103 | var rawMap map[string]json.RawMessage 104 | err = json.NewDecoder(bytes.NewReader(b)).Decode(&rawMap) 105 | assert.Nil(t, err) 106 | assert.NotNil(t, rawMap) 107 | 108 | // Use the "generated" unmarshalDynamicModel function to unmarshal the raw map into a model instance. 109 | var newModel *dynamicModel 110 | err = unmarshalDynamicModel(rawMap, &newModel) 111 | assert.Nil(t, err) 112 | assert.NotNil(t, newModel) 113 | t.Logf("newModel: %+v\n", *newModel) 114 | 115 | // Make sure the new model is the same as the original model. 116 | assert.Equal(t, model, newModel) 117 | } 118 | -------------------------------------------------------------------------------- /core/noauth_authenticator.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | // (C) Copyright IBM Corp. 2019. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | import ( 18 | "net/http" 19 | ) 20 | 21 | // NoAuthAuthenticator is simply a placeholder implementation of the Authenticator interface 22 | // that performs no authentication. This might be useful in testing/debugging situations. 23 | type NoAuthAuthenticator struct { 24 | } 25 | 26 | func NewNoAuthAuthenticator() (*NoAuthAuthenticator, error) { 27 | return &NoAuthAuthenticator{}, nil 28 | } 29 | 30 | func (NoAuthAuthenticator) AuthenticationType() string { 31 | return AUTHTYPE_NOAUTH 32 | } 33 | 34 | func (NoAuthAuthenticator) Validate() error { 35 | return nil 36 | } 37 | 38 | func (this *NoAuthAuthenticator) Authenticate(request *http.Request) error { 39 | // Nothing to do since we're not providing any authentication. 40 | return nil 41 | } 42 | -------------------------------------------------------------------------------- /core/noauth_authenticator_test.go: -------------------------------------------------------------------------------- 1 | //go:build all || fast || auth 2 | 3 | package core 4 | 5 | // (C) Copyright IBM Corp. 2019. 6 | // 7 | // Licensed under the Apache License, Version 2.0 (the "License"); 8 | // you may not use this file except in compliance with the License. 9 | // You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, software 14 | // distributed under the License is distributed on an "AS IS" BASIS, 15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | // See the License for the specific language governing permissions and 17 | // limitations under the License. 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/stretchr/testify/assert" 23 | ) 24 | 25 | func TestNoAuthAuthenticate(t *testing.T) { 26 | // Create a BasicAuthenticator instance from this config. 27 | authenticator, err := NewNoAuthAuthenticator() 28 | assert.Nil(t, err) 29 | assert.NotNil(t, authenticator) 30 | assert.Equal(t, authenticator.AuthenticationType(), AUTHTYPE_NOAUTH) 31 | 32 | // Create a new Request object. 33 | builder, err := NewRequestBuilder("GET").ConstructHTTPURL("https://localhost/placeholder/url", nil, nil) 34 | assert.Nil(t, err) 35 | 36 | request, err := builder.Build() 37 | assert.Nil(t, err) 38 | assert.NotNil(t, request) 39 | 40 | // Test the "Authenticate" method to make sure the Authorization header is not added to the Request. 41 | _ = authenticator.Authenticate(request) 42 | assert.Equal(t, request.Header.Get("Authorization"), "") 43 | } 44 | -------------------------------------------------------------------------------- /core/ordered_maps.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | // (C) Copyright IBM Corp. 2024. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | import ( 18 | yaml "sigs.k8s.io/yaml/goyaml.v2" 19 | ) 20 | 21 | // OrderedMaps provides a wrapper around the yaml package's 22 | // MapItem type and provides convenience functionality beyond 23 | // what would be available with the MapSlice type, which is similar. 24 | // It enables the ordering of fields in a map for controlling the 25 | // order of keys in the printed YAML strings. 26 | type OrderedMaps struct { 27 | maps []yaml.MapItem 28 | } 29 | 30 | // Add appends a key/value pair to the ordered list of maps. 31 | func (m *OrderedMaps) Add(key string, value interface{}) { 32 | m.maps = append(m.maps, yaml.MapItem{ 33 | Key: key, 34 | Value: value, 35 | }) 36 | } 37 | 38 | // GetMaps returns the actual list of ordered maps stored in 39 | // the OrderedMaps instance. Each element is MapItem type, 40 | // which will be serialized by the yaml package in a special 41 | // way that allows the ordering of fields in the YAML. 42 | func (m *OrderedMaps) GetMaps() []yaml.MapItem { 43 | return m.maps 44 | } 45 | 46 | // NewOrderedMaps initializes and returns a new instance of OrderedMaps. 47 | func NewOrderedMaps() *OrderedMaps { 48 | return &OrderedMaps{} 49 | } 50 | -------------------------------------------------------------------------------- /core/ordered_maps_test.go: -------------------------------------------------------------------------------- 1 | //go:build all || fast 2 | 3 | package core 4 | 5 | // (C) Copyright IBM Corp. 2024. 6 | // 7 | // Licensed under the Apache License, Version 2.0 (the "License"); 8 | // you may not use this file except in compliance with the License. 9 | // You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, software 14 | // distributed under the License is distributed on an "AS IS" BASIS, 15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | // See the License for the specific language governing permissions and 17 | // limitations under the License. 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/stretchr/testify/assert" 23 | ) 24 | 25 | func TestNewOrderedMaps(t *testing.T) { 26 | om := NewOrderedMaps() 27 | assert.NotNil(t, om) 28 | assert.Equal(t, 0, len(om.maps)) 29 | } 30 | 31 | func TestOrderedMapsAdd(t *testing.T) { 32 | om := &OrderedMaps{} 33 | assert.Equal(t, 0, len(om.maps)) 34 | 35 | om.Add("key", "value") 36 | assert.Equal(t, 1, len(om.maps)) 37 | assert.Equal(t, om.maps[0].Key, "key") 38 | assert.Equal(t, om.maps[0].Value, "value") 39 | } 40 | 41 | func TestOrderedMapsGetMaps(t *testing.T) { 42 | om := &OrderedMaps{} 43 | om.Add("key", "value") 44 | 45 | maps := om.GetMaps() 46 | assert.Equal(t, 1, len(maps)) 47 | assert.Equal(t, maps[0].Key, "key") 48 | assert.Equal(t, maps[0].Value, "value") 49 | } 50 | -------------------------------------------------------------------------------- /core/parameterized_url.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | // (C) Copyright IBM Corp. 2021. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | import ( 18 | "fmt" 19 | "sort" 20 | "strings" 21 | ) 22 | 23 | // ConstructServiceURL returns a service URL that is constructed by formatting a parameterized URL. 24 | // 25 | // Parameters: 26 | // 27 | // parameterizedUrl: URL that contains variable placeholders, e.g. "{scheme}://ibm.com". 28 | // 29 | // defaultUrlVariables: map from variable names to default values. 30 | // 31 | // Each variable in the parameterized URL must have a default value specified in this map. 32 | // 33 | // providedUrlVariables: map from variable names to desired values. 34 | // 35 | // If a variable is not provided in this map, 36 | // the default variable value will be used instead. 37 | func ConstructServiceURL( 38 | parameterizedUrl string, 39 | defaultUrlVariables map[string]string, 40 | providedUrlVariables map[string]string, 41 | ) (string, error) { 42 | GetLogger().Debug("Constructing service URL from parameterized URL: %s\n", parameterizedUrl) 43 | 44 | // Verify the provided variable names. 45 | for providedName := range providedUrlVariables { 46 | if _, ok := defaultUrlVariables[providedName]; !ok { 47 | // Get all accepted variable names (the keys of the default variables map). 48 | var acceptedNames []string 49 | for name := range defaultUrlVariables { 50 | acceptedNames = append(acceptedNames, name) 51 | } 52 | sort.Strings(acceptedNames) 53 | 54 | return "", fmt.Errorf( 55 | "'%s' is an invalid variable name.\nValid variable names: %s.", 56 | providedName, 57 | acceptedNames, 58 | ) 59 | } 60 | } 61 | 62 | // Format the URL with provided or default variable values. 63 | formattedUrl := parameterizedUrl 64 | 65 | for name, defaultValue := range defaultUrlVariables { 66 | providedValue, ok := providedUrlVariables[name] 67 | 68 | // Use the default variable value if none was provided. 69 | if !ok { 70 | providedValue = defaultValue 71 | } 72 | formattedUrl = strings.Replace(formattedUrl, "{"+name+"}", providedValue, 1) 73 | } 74 | GetLogger().Debug("Returning service URL: %s\n", formattedUrl) 75 | return formattedUrl, nil 76 | } 77 | -------------------------------------------------------------------------------- /core/parameterized_url_test.go: -------------------------------------------------------------------------------- 1 | //go:build all || fast 2 | 3 | package core 4 | 5 | // (C) Copyright IBM Corp. 2021. 6 | // 7 | // Licensed under the Apache License, Version 2.0 (the "License"); 8 | // you may not use this file except in compliance with the License. 9 | // You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, software 14 | // distributed under the License is distributed on an "AS IS" BASIS, 15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | // See the License for the specific language governing permissions and 17 | // limitations under the License. 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/stretchr/testify/assert" 23 | ) 24 | 25 | const parameterizedUrl = "{scheme}://{domain}:{port}" 26 | 27 | var defaultUrlVariables = map[string]string{ 28 | "scheme": "http", 29 | "domain": "ibm.com", 30 | "port": "9300", 31 | } 32 | 33 | func TestConstructServiceURLWithNil(t *testing.T) { 34 | url, err := ConstructServiceURL(parameterizedUrl, defaultUrlVariables, nil) 35 | 36 | assert.Equal(t, url, "http://ibm.com:9300") 37 | assert.Nil(t, err) 38 | } 39 | 40 | func TestConstructServiceURLWithSomeProvidedVariables(t *testing.T) { 41 | providedUrlVariables := map[string]string{ 42 | "scheme": "https", 43 | "port": "22", 44 | } 45 | 46 | url, err := ConstructServiceURL(parameterizedUrl, defaultUrlVariables, providedUrlVariables) 47 | 48 | assert.Equal(t, url, "https://ibm.com:22") 49 | assert.Nil(t, err) 50 | } 51 | 52 | func TestConstructServiceURLWithAllProvidedVariables(t *testing.T) { 53 | var providedUrlVariables = map[string]string{ 54 | "scheme": "https", 55 | "domain": "google.com", 56 | "port": "22", 57 | } 58 | 59 | url, err := ConstructServiceURL(parameterizedUrl, defaultUrlVariables, providedUrlVariables) 60 | 61 | assert.Equal(t, url, "https://google.com:22") 62 | assert.Nil(t, err) 63 | } 64 | 65 | func TestConstructServiceURLWithInvalidVariable(t *testing.T) { 66 | var providedUrlVariables = map[string]string{ 67 | "server": "value", 68 | } 69 | 70 | url, err := ConstructServiceURL(parameterizedUrl, defaultUrlVariables, providedUrlVariables) 71 | 72 | assert.Equal(t, url, "") 73 | assert.EqualError( 74 | t, 75 | err, 76 | "'server' is an invalid variable name.\nValid variable names: [domain port scheme].", 77 | ) 78 | } 79 | -------------------------------------------------------------------------------- /core/problem.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | // (C) Copyright IBM Corp. 2024. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | // Problem is an interface that describes the common 18 | // behavior of custom IBM problem message types. 19 | type Problem interface { 20 | 21 | // GetConsoleMessage returns a message suited to the practitioner 22 | // or end user. It should tell the user what went wrong, and why, 23 | // without unnecessary implementation details. 24 | GetConsoleMessage() string 25 | 26 | // GetDebugMessage returns a message suited to the developer, in 27 | // order to assist in debugging. It should give enough information 28 | // for the developer to identify the root cause of the issue. 29 | GetDebugMessage() string 30 | 31 | // GetID returns an identifier or code for a given problem. It is computed 32 | // from the attributes of the problem, so that the same problem scenario 33 | // will always have the same ID, even when encountered by different users. 34 | GetID() string 35 | 36 | // Error returns the message associated with a given problem and guarantees 37 | // every instance of Problem also implements the native "error" interface. 38 | Error() string 39 | } 40 | 41 | // OrderableProblem provides an interface for retrieving ordered 42 | // representations of problems in order to print YAML messages 43 | // with a controlled ordering of the fields. 44 | type OrderableProblem interface { 45 | GetConsoleOrderedMaps() *OrderedMaps 46 | GetDebugOrderedMaps() *OrderedMaps 47 | } 48 | -------------------------------------------------------------------------------- /core/problem_utils.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | // (C) Copyright IBM Corp. 2024. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | import ( 18 | "crypto/sha256" 19 | "encoding/hex" 20 | "errors" 21 | "fmt" 22 | "strings" 23 | 24 | yaml "sigs.k8s.io/yaml/goyaml.v2" 25 | ) 26 | 27 | func ComputeConsoleMessage(o OrderableProblem) string { 28 | return getProblemInfoAsYAML(o.GetConsoleOrderedMaps()) 29 | } 30 | 31 | func ComputeDebugMessage(o OrderableProblem) string { 32 | return getProblemInfoAsYAML(o.GetDebugOrderedMaps()) 33 | } 34 | 35 | // CreateIDHash computes a unique ID based on a given prefix 36 | // and problem attribute fields. 37 | func CreateIDHash(prefix string, fields ...string) string { 38 | signature := strings.Join(fields, "") 39 | hash := sha256.Sum256([]byte(signature)) 40 | return fmt.Sprintf("%s-%s", prefix, hex.EncodeToString(hash[:4])) 41 | } 42 | 43 | // getProblemInfoAsYAML formats the ordered problem data as 44 | // YAML for human/machine readable printing. 45 | func getProblemInfoAsYAML(orderedMaps *OrderedMaps) string { 46 | asYaml, err := yaml.Marshal(orderedMaps.GetMaps()) 47 | 48 | if err != nil { 49 | return fmt.Sprintf("Error serializing the problem information: %s", err.Error()) 50 | } 51 | return fmt.Sprintf("---\n%s---\n", asYaml) 52 | } 53 | 54 | // getComponentInfo is a convenient way to access the name of the 55 | // component alongside the current semantic version of the component. 56 | func getComponentInfo() *ProblemComponent { 57 | return NewProblemComponent(MODULE_NAME, __VERSION__) 58 | } 59 | 60 | // is provides a simple utility function that assists problem types 61 | // implement an "Is" function for checking error equality. Error types 62 | // are treated as equivalent if they are both Problem types and their 63 | // IDs match. 64 | func is(target error, id string) bool { 65 | var problem Problem 66 | return errors.As(target, &problem) && problem.GetID() == id 67 | } 68 | -------------------------------------------------------------------------------- /core/problem_utils_test.go: -------------------------------------------------------------------------------- 1 | //go:build all || fast || problem 2 | 3 | package core 4 | 5 | // (C) Copyright IBM Corp. 2024. 6 | // 7 | // Licensed under the Apache License, Version 2.0 (the "License"); 8 | // you may not use this file except in compliance with the License. 9 | // You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, software 14 | // distributed under the License is distributed on an "AS IS" BASIS, 15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | // See the License for the specific language governing permissions and 17 | // limitations under the License. 18 | 19 | import ( 20 | "fmt" 21 | "testing" 22 | 23 | "github.com/stretchr/testify/assert" 24 | ) 25 | 26 | const ( 27 | consoleKey = "console" 28 | consoleValue = "my-console-message" 29 | debugKey = "debug" 30 | debugValue = "my-debug-message" 31 | messageTemplate = "---\n%s: %s\n---\n" 32 | ) 33 | 34 | func TestComputeConsoleMessage(t *testing.T) { 35 | message := ComputeConsoleMessage(&MockOrderableProblem{}) 36 | expected := fmt.Sprintf(messageTemplate, consoleKey, consoleValue) 37 | assert.Equal(t, expected, message) 38 | } 39 | 40 | func TestComputeDebugMessage(t *testing.T) { 41 | message := ComputeDebugMessage(&MockOrderableProblem{}) 42 | expected := fmt.Sprintf(messageTemplate, debugKey, debugValue) 43 | assert.Equal(t, expected, message) 44 | } 45 | 46 | func TestCreateIDHash(t *testing.T) { 47 | hash := CreateIDHash("my-prefix", "component", "discriminator") 48 | assert.Equal(t, "my-prefix-9507ef8a", hash) 49 | 50 | hash = CreateIDHash("other-prefix", "component", "discriminator", "function", "caused_by_id") 51 | assert.Equal(t, "other-prefix-f24346b0", hash) 52 | } 53 | 54 | func TestGetProblemInfoAsYAML(t *testing.T) { 55 | mockOP := &MockOrderableProblem{} 56 | message := getProblemInfoAsYAML(mockOP.GetConsoleOrderedMaps()) 57 | expected := fmt.Sprintf(messageTemplate, consoleKey, consoleValue) 58 | assert.Equal(t, expected, message) 59 | } 60 | 61 | func TestGetComponentInfo(t *testing.T) { 62 | component := getComponentInfo() 63 | assert.NotNil(t, component) 64 | assert.Equal(t, MODULE_NAME, component.Name) 65 | assert.Equal(t, __VERSION__, component.Version) 66 | } 67 | 68 | type MockOrderableProblem struct{} 69 | 70 | func (m *MockOrderableProblem) GetConsoleOrderedMaps() *OrderedMaps { 71 | orderedMaps := NewOrderedMaps() 72 | orderedMaps.Add(consoleKey, consoleValue) 73 | return orderedMaps 74 | } 75 | 76 | func (m *MockOrderableProblem) GetDebugOrderedMaps() *OrderedMaps { 77 | orderedMaps := NewOrderedMaps() 78 | orderedMaps.Add(debugKey, debugValue) 79 | return orderedMaps 80 | } 81 | -------------------------------------------------------------------------------- /core/sdk_problem.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | // (C) Copyright IBM Corp. 2024. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | import ( 18 | "errors" 19 | ) 20 | 21 | // SDKProblem provides a type suited to problems that 22 | // occur in SDK projects. It extends the base 23 | // "IBMProblem" type with a field to store the 24 | // function being called when the problem occurs. 25 | type SDKProblem struct { 26 | *IBMProblem 27 | 28 | // Function provides the name of the in-code 29 | // function or method in which the problem 30 | // occurred. 31 | Function string 32 | 33 | // A computed stack trace including the relevant 34 | // function names, files, and line numbers invoked 35 | // leading up to the origination of the problem. 36 | stack []sdkStackFrame 37 | 38 | // If the problem instance originated in the core, we 39 | // want to keep track of the information for debugging 40 | // purposes (even though we aren't using the problem 41 | // as a "caused by" problem). 42 | coreProblem *sparseSDKProblem 43 | 44 | // If the problem instance originated in the core and 45 | // was caused by an HTTP request, we don't use the 46 | // HTTPProblem instance as a "caused by" but we need 47 | // to store the instance to pass it to a downstream 48 | // SDKProblem instance. 49 | httpProblem *HTTPProblem 50 | } 51 | 52 | // GetConsoleMessage returns all public fields of 53 | // the problem, formatted in YAML. 54 | func (e *SDKProblem) GetConsoleMessage() string { 55 | return ComputeConsoleMessage(e) 56 | } 57 | 58 | // GetDebugMessage returns all information 59 | // about the problem, formatted in YAML. 60 | func (e *SDKProblem) GetDebugMessage() string { 61 | return ComputeDebugMessage(e) 62 | } 63 | 64 | // GetID returns the computed identifier, computed from the 65 | // "Component", "discriminator", and "Function" fields, as well as the 66 | // identifier of the "causedBy" problem, if it exists. 67 | func (e *SDKProblem) GetID() string { 68 | return CreateIDHash("sdk", e.GetBaseSignature(), e.Function) 69 | } 70 | 71 | // Is allows an SDKProblem instance to be compared against another error for equality. 72 | // An SDKProblem is considered equal to another error if 1) the error is also a Problem and 73 | // 2) it has the same ID (i.e. it is the same problem scenario). 74 | func (e *SDKProblem) Is(target error) bool { 75 | return is(target, e.GetID()) 76 | } 77 | 78 | // GetConsoleOrderedMaps returns an ordered-map representation 79 | // of an SDKProblem instance suited for a console message. 80 | func (e *SDKProblem) GetConsoleOrderedMaps() *OrderedMaps { 81 | orderedMaps := NewOrderedMaps() 82 | 83 | orderedMaps.Add("id", e.GetID()) 84 | orderedMaps.Add("summary", e.Summary) 85 | orderedMaps.Add("severity", e.Severity) 86 | orderedMaps.Add("function", e.Function) 87 | orderedMaps.Add("component", e.Component) 88 | 89 | return orderedMaps 90 | } 91 | 92 | // GetDebugOrderedMaps returns an ordered-map representation 93 | // of an SDKProblem instance, with additional information 94 | // suited for a debug message. 95 | func (e *SDKProblem) GetDebugOrderedMaps() *OrderedMaps { 96 | orderedMaps := e.GetConsoleOrderedMaps() 97 | 98 | orderedMaps.Add("stack", e.stack) 99 | 100 | if e.coreProblem != nil { 101 | orderedMaps.Add("core_problem", e.coreProblem) 102 | } 103 | 104 | var orderableCausedBy OrderableProblem 105 | if errors.As(e.GetCausedBy(), &orderableCausedBy) { 106 | orderedMaps.Add("caused_by", orderableCausedBy.GetDebugOrderedMaps().GetMaps()) 107 | } 108 | 109 | return orderedMaps 110 | } 111 | 112 | // SDKErrorf creates and returns a new instance of "SDKProblem" with "error" level severity. 113 | func SDKErrorf(err error, summary, discriminator string, component *ProblemComponent) *SDKProblem { 114 | function := computeFunctionName(component.Name) 115 | stack := getStackInfo(component.Name) 116 | 117 | ibmProb := IBMErrorf(err, component, summary, discriminator) 118 | newSDKProb := &SDKProblem{ 119 | IBMProblem: ibmProb, 120 | Function: function, 121 | stack: stack, 122 | } 123 | 124 | // Flatten chains of SDKProblem instances in order to present a single, 125 | // unique error scenario for the SDK context. Multiple Golang components 126 | // can invoke each other, but we only want to track "caused by" problems 127 | // through context boundaries (like API, SDK, Terraform, etc.). This 128 | // eliminates unnecessary granularity of problem scenarios for the SDK 129 | // context. If the problem originated in this library (the Go SDK Core), 130 | // we still want to track that info for debugging purposes. 131 | var sdkCausedBy *SDKProblem 132 | if errors.As(err, &sdkCausedBy) { 133 | // Not a "native" caused by but allows us to maintain compatibility through "Unwrap". 134 | newSDKProb.nativeCausedBy = sdkCausedBy 135 | newSDKProb.causedBy = nil 136 | 137 | if isCoreProblem(sdkCausedBy) { 138 | newSDKProb.coreProblem = newSparseSDKProblem(sdkCausedBy) 139 | 140 | // If we stored an HTTPProblem instance in the core, we'll want to use 141 | // it as the actual "caused by" problem for the new SDK problem. 142 | if sdkCausedBy.httpProblem != nil { 143 | newSDKProb.causedBy = sdkCausedBy.httpProblem 144 | } 145 | } 146 | } 147 | 148 | // We can't use HTTPProblem instances as "caused by" problems for Go SDK Core 149 | // problems because 1) it prevents us from enumerating hashes in the core and 150 | // 2) core problems will almost never be the instances that users interact with 151 | // and the HTTPProblem will need to be used as the "caused by" of the problems 152 | // coming from actual service SDK libraries. 153 | var httpCausedBy *HTTPProblem 154 | if errors.As(err, &httpCausedBy) && isCoreProblem(newSDKProb) { 155 | newSDKProb.httpProblem = httpCausedBy 156 | newSDKProb.causedBy = nil 157 | } 158 | 159 | return newSDKProb 160 | } 161 | 162 | // RepurposeSDKProblem provides a convenient way to take a problem from 163 | // another function in the same component and contextualize it to the current 164 | // function. Should only be used in public (exported) functions. 165 | func RepurposeSDKProblem(err error, discriminator string) error { 166 | if err == nil { 167 | return nil 168 | } 169 | 170 | // It only makes sense to carry out this logic with SDK Errors. 171 | var sdkErr *SDKProblem 172 | if !errors.As(err, &sdkErr) { 173 | return err 174 | } 175 | 176 | // Special behavior to allow SDK problems coming from a method that wraps a 177 | // "*WithContext" method to maintain the discriminator of the originating 178 | // problem. Otherwise, we would lose all of that data in the wrap. 179 | if discriminator != "" { 180 | sdkErr.discriminator = discriminator 181 | } 182 | 183 | // Recompute the function to reflect this public boundary (but let the stack 184 | // remain as it is - it is the path to the original problem origination point). 185 | sdkErr.Function = computeFunctionName(sdkErr.Component.Name) 186 | 187 | return sdkErr 188 | } 189 | -------------------------------------------------------------------------------- /core/sdk_problem_utils.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | // (C) Copyright IBM Corp. 2024. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | import ( 18 | "runtime" 19 | "strings" 20 | ) 21 | 22 | // computeFunctionName investigates the program counter at a fixed 23 | // skip number (aka point in the stack) of 2, which gives us the 24 | // information about the function the problem was created in, and 25 | // returns the name of the function. 26 | func computeFunctionName(componentName string) string { 27 | if pc, _, _, ok := runtime.Caller(2); ok { 28 | // The function names will have the component name as a prefix. 29 | // To avoid redundancy, since we are including the component name 30 | // with the problem, trim that prefix here. 31 | return strings.TrimPrefix(runtime.FuncForPC(pc).Name(), componentName+"/") 32 | } 33 | 34 | return "" 35 | } 36 | 37 | // sdkStackFrame is a convenience struct for formatting 38 | // frame data to be printed as YAML. 39 | type sdkStackFrame struct { 40 | Function string 41 | File string 42 | Line int 43 | } 44 | 45 | // getStackInfo invokes helper methods to curate a limited, formatted 46 | // version of the stack trace with only the component-scoped function 47 | // invocations that lead to the creation of the problem. 48 | func getStackInfo(componentName string) []sdkStackFrame { 49 | if frames, ok := makeFrames(); ok { 50 | return formatFrames(frames, componentName) 51 | } 52 | 53 | return nil 54 | } 55 | 56 | // makeFrames populates a program counter list with data at a 57 | // fixed skip number (4), which gives us the stack information 58 | // at the point in the program that the problem was created. This 59 | // function adjusts the list as needed, since the necessary 60 | // list size is not known at first. 61 | func makeFrames() ([]uintptr, bool) { 62 | pcs := make([]uintptr, 10) 63 | for { 64 | n := runtime.Callers(4, pcs) 65 | if n == 0 { 66 | return pcs, false 67 | } 68 | if n < len(pcs) { 69 | return pcs[:n], true 70 | } 71 | pcs = make([]uintptr, 2*len(pcs)) 72 | } 73 | } 74 | 75 | // formatFrames takes a program counter list and formats them 76 | // into a readable format for including in debug messages. 77 | func formatFrames(pcs []uintptr, componentName string) []sdkStackFrame { 78 | result := make([]sdkStackFrame, 0) 79 | 80 | if len(pcs) == 0 { 81 | return result 82 | } 83 | 84 | // Loop to get frames. 85 | // A fixed number of PCs can expand to an indefinite number of Frames. 86 | frames := runtime.CallersFrames(pcs) 87 | for { 88 | frame, more := frames.Next() 89 | 90 | // Only the frames in the same component as the problem are relevant. 91 | if strings.HasPrefix(frame.Function, componentName) { 92 | stackFrame := sdkStackFrame{ 93 | Function: frame.Function, 94 | File: frame.File, 95 | Line: frame.Line, 96 | } 97 | 98 | result = append(result, stackFrame) 99 | } 100 | 101 | // Check whether there are more frames to process after this one. 102 | if !more { 103 | break 104 | } 105 | } 106 | 107 | return result 108 | } 109 | 110 | type sparseSDKProblem struct { 111 | ID string 112 | Function string 113 | } 114 | 115 | func newSparseSDKProblem(prob *SDKProblem) *sparseSDKProblem { 116 | return &sparseSDKProblem{ 117 | ID: prob.GetID(), 118 | Function: prob.Function, 119 | } 120 | } 121 | 122 | func isCoreProblem(prob *SDKProblem) bool { 123 | return prob.Component != nil && prob.Component.Name == MODULE_NAME 124 | } 125 | -------------------------------------------------------------------------------- /core/version.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | // (C) Copyright IBM Corp. 2019. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | // Version of the SDK 18 | const __VERSION__ = "5.20.0" 19 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/IBM/go-sdk-core/v5 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.6 6 | 7 | require ( 8 | github.com/go-openapi/strfmt v0.23.0 9 | github.com/go-playground/validator/v10 v10.26.0 10 | github.com/hashicorp/go-cleanhttp v0.5.2 11 | github.com/hashicorp/go-retryablehttp v0.7.7 12 | github.com/onsi/ginkgo v1.16.5 13 | github.com/onsi/gomega v1.37.0 14 | github.com/stretchr/testify v1.10.0 15 | sigs.k8s.io/yaml v1.4.0 16 | ) 17 | 18 | require ( 19 | github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect 20 | github.com/davecgh/go-spew v1.1.1 // indirect 21 | github.com/fsnotify/fsnotify v1.6.0 // indirect 22 | github.com/gabriel-vasile/mimetype v1.4.8 // indirect 23 | github.com/go-openapi/errors v0.22.0 // indirect 24 | github.com/go-playground/locales v0.14.1 // indirect 25 | github.com/go-playground/universal-translator v0.18.1 // indirect 26 | github.com/google/go-cmp v0.7.0 // indirect 27 | github.com/google/uuid v1.6.0 // indirect 28 | github.com/leodido/go-urn v1.4.0 // indirect 29 | github.com/mitchellh/mapstructure v1.5.0 // indirect 30 | github.com/nxadm/tail v1.4.8 // indirect 31 | github.com/oklog/ulid v1.3.1 // indirect 32 | github.com/pmezard/go-difflib v1.0.0 // indirect 33 | go.mongodb.org/mongo-driver v1.17.2 // indirect 34 | golang.org/x/crypto v0.36.0 // indirect 35 | golang.org/x/net v0.38.0 // indirect 36 | golang.org/x/sys v0.31.0 // indirect 37 | golang.org/x/text v0.23.0 // indirect 38 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect 39 | gopkg.in/yaml.v3 v3.0.1 // indirect 40 | ) 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "semantic-release-dependencies", 3 | "version": "0.0.0", 4 | "description": "This package.json is being used to manage semantic-release and its dependencies", 5 | "license": "Apache-2.0", 6 | "devDependencies": { 7 | "semantic-release": "21.0.7", 8 | "@semantic-release/changelog": "6.0.3", 9 | "@semantic-release/exec": "6.0.3", 10 | "@semantic-release/git": "10.0.1" 11 | }, 12 | "overrides": { 13 | "semver": "^7.5.3" 14 | }, 15 | "scripts": { 16 | "semantic-release": "semantic-release" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /resources/cr-token.txt: -------------------------------------------------------------------------------- 1 | cr-token-1 -------------------------------------------------------------------------------- /resources/empty-cr-token.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/go-sdk-core/60ac9f39b2b91880f40751e424b8741f39f7c721/resources/empty-cr-token.txt -------------------------------------------------------------------------------- /resources/ibm-credentials.env: -------------------------------------------------------------------------------- 1 | # 2 | MY_SERVICE1_URL=https://cp4d.com 3 | MY_SERVICE1_AUTH_TYPE=cp4d 4 | MY_SERVICE1_AUTH_URL=https://cp4d.com 5 | MY_SERVICE1_USERNAME=foo 6 | MY_SERVICE1_PASSWORD=bar 7 | # 8 | MY_SERVICE2_URL=https://basicurl 9 | MY_SERVICE2_AUTH_TYPE=basic 10 | MY_SERVICE2_USERNAME=user1 11 | MY_SERVICE2_PASSWORD=password1 12 | # 13 | MY_SERVICE3_URL=https://basicurl 14 | MY_SERVICE3_AUTH_TYPE=basic 15 | MY_SERVICE3_USERNAME=user1 16 | MY_SERVICE3_PASSWORD=password1 17 | # 18 | MY_SERVICE4_APIKEY=5678efgh 19 | MY_SERVICE4_USERNAME=test 20 | MY_SERVICE4_PASSWORD=pwd 21 | MY_SERVICE4_URL=https://api.us-south.assistant.watson.cloud.ibm.com 22 | # 23 | MY_SERVICE5_AUTH_TYPE=bearerToken 24 | MY_SERVICE5_USERNAME=test 25 | MY_SERVICE5_PASSWORD=pwd 26 | MY_SERVICE5_URL=https://api.us-south.assistant.watson.cloud.ibm.com 27 | -------------------------------------------------------------------------------- /resources/my-credentials.env: -------------------------------------------------------------------------------- 1 | # Service-specific properties not related to authentication. 2 | SERVICE_1_URL=https://service1/api 3 | SERVICE_1_DISABLE_SSL=true 4 | SERVICE_1_ENABLE_GZIP=true 5 | 6 | SERVICE2_URL=https://service2/api 7 | SERVICE2_DISABLE_SSL=false 8 | SERVICE2_ENABLE_GZIP=false 9 | SERVICE2_ENABLE_RETRIES=true 10 | 11 | SERVICE3_URL=https://service3/api 12 | SERVICE3_DISABLE_SSL=false 13 | SERVICE3_ENABLE_GZIP=notabool 14 | SERVICE3_ENABLE_RETRIES=notabool 15 | 16 | SERVICE4_URL=https://service4/api 17 | SERVICE4_DISABLE_SSL=false 18 | SERVICE4_ENABLE_RETRIES=true 19 | SERVICE4_MAX_RETRIES=5 20 | SERVICE4_RETRY_INTERVAL=10 21 | 22 | SERVICE5_URL=https://service5/api 23 | SERVICE5_DISABLE_SSL=true 24 | 25 | # Service-1 configured with IAM 26 | SERVICE_1_AUTH_TYPE=IAM 27 | SERVICE_1_APIKEY=my-api-key 28 | SERVICE_1_CLIENT_ID=my-client-id 29 | SERVICE_1_CLIENT_SECRET=my-client-secret 30 | SERVICE_1_AUTH_URL=https://iamhost/iam/api 31 | SERVICE_1_AUTH_DISABLE_SSL=true 32 | 33 | # Service2 configured with Basic Auth 34 | SERVICE2_AUTHTYPE=BasiC 35 | SERVICE2_USERNAME=my-user 36 | SERVICE2_PASSWORD=my-password 37 | 38 | # Service3 configured with CP4D 39 | SERVICE3_AUTHTYPE=CP4d 40 | SERVICE3_AUTH_URL=https://cp4dhost/cp4d/api 41 | SERVICE3_USERNAME=my-cp4d-user 42 | SERVICE3_PASSWORD=my-cp4d-password 43 | SERVICE3_AUTH_DISABLE_SSL=false 44 | 45 | # Service4 configured with no authentication 46 | SERVICE4_AUTH_TYPE=NOAuth 47 | 48 | # Service5 configured with BearerToken 49 | SERVICE5_AUTH_TYPE=BEARERtoken 50 | SERVICE5_BEARER_TOKEN=my-bearer-token 51 | 52 | # Service6 configured with IAM w/scope 53 | SERVICE6_AUTH_TYPE=IAM 54 | SERVICE6_APIKEY=my-api-key 55 | SERVICE6_AUTH_URL=https://iamhost/iam/api 56 | SERVICE6_SCOPE=scope1 scope2 scope3 57 | 58 | # Service configured with Container Auth 59 | SERVICE7_AUTH_TYPE=conTaIneR 60 | SERVICE7_CR_TOKEN_FILENAME=crtoken.txt 61 | SERVICE7_IAM_PROFILE_NAME=iam-user1 62 | SERVICE7_IAM_PROFILE_ID=iam-id1 63 | SERVICE7_AUTH_URL=https://iamhost/iam/api 64 | SERVICE7_SCOPE=scope1 65 | SERVICE7_CLIENT_ID=iam-client1 66 | SERVICE7_CLIENT_SECRET=iam-secret1 67 | SERVICE7_AUTH_DISABLE_SSL=true 68 | 69 | # VPC auth with default config 70 | SERVICE8A_AUTH_TYPE=vpc 71 | 72 | # VPC auth with profile CRN 73 | SERVICE8B_AUTH_TYPE=vpc 74 | SERVICE8B_IAM_PROFILE_CRN=crn:iam-profile1 75 | SERVICE8B_AUTH_URL=http://vpc.imds.com/api 76 | 77 | # VPC auth with profile ID 78 | SERVICE8C_AUTH_TYPE=vpc 79 | SERVICE8C_IAM_PROFILE_ID=iam-profile1-id 80 | 81 | # IAM auth using refresh token 82 | SERVICE9_AUTH_TYPE=iam 83 | SERVICE9_REFRESH_TOKEN=refresh-token 84 | SERVICE9_CLIENT_ID=user1 85 | SERVICE9_CLIENT_SECRET=secret1 86 | SERVICE9_AUTH_URL=https://iam.refresh-token.com 87 | 88 | # MCSP auth 89 | SERVICE10_AUTH_TYPE=mcsp 90 | SERVICE10_APIKEY=my-api-key 91 | SERVICE10_AUTH_URL=https://mcsp.ibm.com 92 | SERVICE10_AUTH_DISABLE_SSL=true 93 | 94 | SERVICE11_AUTH_TYPE=iAmAsSuME 95 | SERVICE11_APIKEY=my-api-key 96 | SERVICE11_IAM_PROFILE_ID=iam-profile-1 97 | SERVICE11_AUTH_URL=https://iamassume.ibm.com 98 | SERVICE11_AUTH_DISABLE_SSL=true 99 | 100 | # MCSP V2 auth 101 | SERVICE12_AUTH_TYPE=mcspv2 102 | SERVICE12_APIKEY=my-api-key 103 | SERVICE12_AUTH_URL=https://mcspv2.ibm.com 104 | SERVICE12_SCOPE_COLLECTION_TYPE=subscriptions 105 | SERVICE12_SCOPE_ID=global_subscriptions 106 | SERVICE12_INCLUDE_BUILTIN_ACTIONS=TRUE 107 | SERVICE12_INCLUDE_CUSTOM_ACTIONS=t 108 | SERVICE12_INCLUDE_ROLES=f 109 | SERVICE12_PREFIX_ROLES=true 110 | SERVICE12_CALLER_EXT_CLAIM={"productID":"prod123"} 111 | SERVICE12_AUTH_DISABLE_SSL=true 112 | 113 | # EQUAL service exercises value with = in them 114 | EQUAL_SERVICE_URL==https:/my=host.com/my=service/api 115 | EQUAL_SERVICE_APIKEY==my=api=key= 116 | 117 | # Error1 - missing APIKEY 118 | ERROR1_AUTH_TYPE=iaM 119 | 120 | # Error2 - missing username 121 | ERROR2_AUTH_TYPE=baSIC 122 | ERROR2_PASSWORD=password 123 | 124 | # Error3 - missing access token 125 | ERROR3_AUTH_TYPE=bearerTOKEN 126 | ERROR3_BEARER_TOKEN= 127 | 128 | # Error4 - invalid service URL 129 | ERROR4_URL={bad url} -------------------------------------------------------------------------------- /resources/test_file.txt: -------------------------------------------------------------------------------- 1 | hello world from text file -------------------------------------------------------------------------------- /resources/vcap_services.json: -------------------------------------------------------------------------------- 1 | { 2 | "service-1": [ 3 | { 4 | "credentials": { 5 | "url": "https://service1/api", 6 | "username": "my-vcap-user", 7 | "password": "my-vcap-password", 8 | "apikey": "my-vcap-apikey1" 9 | } 10 | } 11 | ], 12 | "service2": [ 13 | { 14 | "credentials": { 15 | "url": "https://service2/api", 16 | "username": "my-vcap-user", 17 | "password": "my-vcap-password" 18 | } 19 | } 20 | ], 21 | "service3": [ 22 | { 23 | "credentials": { 24 | "url": "https://service3/api", 25 | "apikey": "my-vcap-apikey3" 26 | } 27 | } 28 | ], 29 | "key_to_service_entry_1": [ 30 | { 31 | "name": "service4", 32 | "credentials": { 33 | "url": "https://service3/api", 34 | "apikey": "my-vcap-apikey3" 35 | } 36 | }, 37 | { 38 | "name": "service_entry_key_and_key_to_service_entries", 39 | "label": "devops-insights", 40 | "plan": "standard", 41 | "credentials": { 42 | "url": "https://on.the.toolchainplatform.net/devops-insights/api", 43 | "username": "not-a-username", 44 | "password": "not-a-password" 45 | } 46 | } 47 | ], 48 | "key_to_service_entry_2": [ 49 | { 50 | "label": "devops-insights", 51 | "plan": "standard", 52 | "credentials": { 53 | "url": "https://on.the.toolchainplatform.net/devops-insights-3/api", 54 | "username": "not-a-username-3", 55 | "password": "not-a-password-3" 56 | } 57 | } 58 | ], 59 | "service_entry_key_and_key_to_service_entries": [ 60 | { 61 | "name": "service_entry_key-2", 62 | "label": "devops-insights", 63 | "plan": "elite", 64 | "credentials": { 65 | "url": "https://on.the.toolchainplatform.net/devops-insights-2/api", 66 | "username": "not-a-username-2", 67 | "password": "not-a-password-2" 68 | } 69 | } 70 | ], 71 | "empty_service": [], 72 | "no-creds-service": [ 73 | { 74 | "name": "no-creds-service", 75 | "label": "devops-insights", 76 | "plan": "elite" 77 | } 78 | ] 79 | } --------------------------------------------------------------------------------