├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── codeql.yml │ └── release.yml ├── .gitignore ├── .mvn └── wrapper │ └── maven-wrapper.properties ├── .sdkmanrc ├── LICENSE ├── README.md ├── docs ├── BACKGROUND.md ├── BUILD.md ├── CLONING.md ├── CONFIGURATION.md ├── ENDPOINTS.md ├── PREREQUISITES.md ├── RUN.md ├── SONARQUBE.md ├── TAS.md ├── TOOLS.md ├── cf-hoover-2024.excalidraw └── cf-hoover-2024.png ├── manifest.with-registry.yml ├── manifest.yml ├── mvnw ├── mvnw.cmd ├── pom.xml ├── samples ├── application-pcfone.yml ├── application-pws.yml └── config-server.json ├── scripts ├── deploy.sh ├── deploy.with-registry.sh ├── destroy.sh └── destroy.with-registry.sh └── src ├── main ├── java │ └── org │ │ └── cftoolsuite │ │ └── cfapp │ │ ├── AppInit.java │ │ ├── client │ │ ├── DemographicsClient.java │ │ ├── OrganizationsClient.java │ │ ├── SnapshotClient.java │ │ ├── SpaceUsersClient.java │ │ ├── SpacesClient.java │ │ ├── SpringApplicationClient.java │ │ ├── TimeKeeperClient.java │ │ └── UsageClient.java │ │ ├── config │ │ ├── HooverSettings.java │ │ └── WebClientConfig.java │ │ ├── controller │ │ ├── DemographicsController.java │ │ ├── OrganizationsController.java │ │ ├── SnapshotController.java │ │ ├── SpaceUsersController.java │ │ ├── SpacesController.java │ │ ├── SpringApplicationController.java │ │ ├── TimeKeeperController.java │ │ └── UsageController.java │ │ ├── domain │ │ ├── AppDetail.java │ │ ├── AppRelationship.java │ │ ├── ApplicationCounts.java │ │ ├── Demographic.java │ │ ├── Demographics.java │ │ ├── JavaAppDetail.java │ │ ├── Organization.java │ │ ├── ServiceInstanceCounts.java │ │ ├── ServiceInstanceDetail.java │ │ ├── SnapshotDetail.java │ │ ├── SnapshotSummary.java │ │ ├── Space.java │ │ ├── SpaceUsers.java │ │ ├── TimeKeeper.java │ │ ├── TimeKeepers.java │ │ └── accounting │ │ │ ├── application │ │ │ ├── AppUsageMonthly.java │ │ │ ├── AppUsageReport.java │ │ │ └── AppUsageYearly.java │ │ │ ├── service │ │ │ ├── ServicePlanUsageMonthly.java │ │ │ ├── ServicePlanUsageYearly.java │ │ │ ├── ServiceUsageMonthly.java │ │ │ ├── ServiceUsageMonthlyAggregate.java │ │ │ ├── ServiceUsageReport.java │ │ │ └── ServiceUsageYearlyAggregate.java │ │ │ └── task │ │ │ ├── TaskUsageMonthly.java │ │ │ ├── TaskUsageReport.java │ │ │ └── TaskUsageYearly.java │ │ ├── report │ │ ├── AppDetailCsvReport.java │ │ ├── AppRelationshipCsvReport.java │ │ └── ServiceInstanceDetailCsvReport.java │ │ └── task │ │ ├── AppDetailRetrievedEvent.java │ │ ├── AppRelationshipRetrievedEvent.java │ │ └── ServiceInstanceDetailRetrievedEvent.java └── resources │ ├── application.yml │ ├── log4j2-spring.xml │ ├── log4j2.component.properties │ └── logback-spring.xml └── test └── java └── org └── cftoolsuite └── cfapp └── CfHooverApplicationTests.java /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: maven 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "13:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Java CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | java: [ 21 ] 11 | name: Java ${{ matrix.java }} build 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Set up Java 15 | uses: actions/setup-java@v4 16 | with: 17 | distribution: liberica 18 | java-version: ${{ matrix.java }} 19 | - name: Cache local Maven repository 20 | uses: actions/cache@v4 21 | with: 22 | path: ~/.m2/repository 23 | key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} 24 | restore-keys: | 25 | ${{ runner.os }}-maven- 26 | - name: Build with Maven 27 | run: ./mvnw --batch-mode --update-snapshots clean verify 28 | 29 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "main" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "main" ] 20 | schedule: 21 | - cron: '35 3 * * 6' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | # Runner size impacts CodeQL analysis time. To learn more, please see: 27 | # - https://gh.io/recommended-hardware-resources-for-running-codeql 28 | # - https://gh.io/supported-runners-and-hardware-resources 29 | # - https://gh.io/using-larger-runners 30 | # Consider using larger runners for possible analysis time improvements. 31 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} 32 | timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} 33 | permissions: 34 | actions: read 35 | contents: read 36 | security-events: write 37 | 38 | strategy: 39 | fail-fast: false 40 | matrix: 41 | language: [ 'java' ] 42 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby', 'swift' ] 43 | # Use only 'java' to analyze code written in Java, Kotlin or both 44 | # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both 45 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 46 | 47 | steps: 48 | - name: Checkout repository 49 | uses: actions/checkout@v4 50 | 51 | - name: Set up Java 52 | uses: actions/setup-java@v4 53 | with: 54 | distribution: liberica 55 | java-version: 21 56 | 57 | - name: Cache local Maven repository 58 | uses: actions/cache@v4 59 | with: 60 | path: ~/.m2/repository 61 | key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} 62 | restore-keys: | 63 | ${{ runner.os }}-maven- 64 | 65 | # Initializes the CodeQL tools for scanning. 66 | - name: Initialize CodeQL 67 | uses: github/codeql-action/init@v2 68 | with: 69 | languages: ${{ matrix.language }} 70 | # If you wish to specify custom queries, you can do so here or in a config file. 71 | # By default, queries listed here will override any specified in a config file. 72 | # Prefix the list here with "+" to use these queries and those in the config file. 73 | 74 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 75 | # queries: security-extended,security-and-quality 76 | 77 | 78 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). 79 | # If this step fails, then you should remove it and run the build manually (see below) 80 | - name: Autobuild 81 | uses: github/codeql-action/autobuild@v2 82 | 83 | # ℹ️ Command-line programs to run using the OS shell. 84 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 85 | 86 | # If the Autobuild fails above, remove it and uncomment the following three lines. 87 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 88 | 89 | # - run: | 90 | # echo "Run, Build Application using script" 91 | # ./location_of_script_within_repo/buildscript.sh 92 | 93 | - name: Perform CodeQL Analysis 94 | uses: github/codeql-action/analyze@v2 95 | with: 96 | category: "/language:${{matrix.language}}" 97 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish package to GitHub Packages 2 | on: 3 | release: 4 | types: [created] 5 | jobs: 6 | publish: 7 | runs-on: ubuntu-latest 8 | permissions: 9 | contents: write 10 | checks: write 11 | actions: read 12 | issues: read 13 | packages: write 14 | pull-requests: read 15 | repository-projects: read 16 | statuses: read 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Set up Java 20 | uses: actions/setup-java@v4 21 | with: 22 | distribution: liberica 23 | java-version: 21 24 | - name: Install GitHub CLI 25 | run: | 26 | sudo apt update && sudo apt install gh -y 27 | - name: Publish package capable of targeting H2 in-memory backend 28 | run: | 29 | ./mvnw versions:set -DnewVersion=${{ github.event.release.tag_name }} && ./mvnw --batch-mode --update-snapshots -Pexpose-runtime-metadata clean deploy 30 | SUFFIX="${{ github.event.release.tag_name }}" 31 | echo "Uploading cf-hoover-$SUFFIX.jar to release ${{ github.event.release.tag_name }}" 32 | gh release upload "${{ github.event.release.tag_name }}" "/home/runner/.m2/repository/org/cftoolsuite/cfapp/cf-hoover/$SUFFIX/cf-hoover-$SUFFIX.jar" --clobber 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Maven ### 2 | target/ 3 | bin/ 4 | 5 | ### STS ### 6 | .apt_generated 7 | .classpath 8 | .factorypath 9 | .project 10 | .settings 11 | .springBeans 12 | 13 | ### IntelliJ IDEA ### 14 | .idea 15 | *.iws 16 | *.iml 17 | *.ipr 18 | 19 | ### NetBeans ### 20 | nbproject/private/ 21 | build/ 22 | nbbuild/ 23 | dist/ 24 | nbdist/ 25 | .nb-gradle/ 26 | logs/ 27 | 28 | ### Visual Studio Code ### 29 | .vscode 30 | .history/ 31 | 32 | ### User config 33 | config/*.json 34 | config/application-*.yml 35 | src/main/resources/application-*.yml -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. 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, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | wrapperVersion=3.3.2 18 | distributionType=only-script 19 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip 20 | -------------------------------------------------------------------------------- /.sdkmanrc: -------------------------------------------------------------------------------- 1 | java=21-graalce -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VMware Tanzu Application Service > Hoover 2 | 3 | [![GA](https://img.shields.io/badge/Release-GA-darkgreen)](https://img.shields.io/badge/Release-GA-darkgreen) ![Github Action CI Workflow Status](https://github.com/cf-toolsuite/cf-hoover/actions/workflows/ci.yml/badge.svg) [![Known Vulnerabilities](https://snyk.io/test/github/cf-toolsuite/cf-hoover/badge.svg?style=plastic)](https://snyk.io/test/github/cf-toolsuite/cf-hoover) [![Release](https://jitpack.io/v/cf-toolsuite/cf-hoover.svg)](https://jitpack.io/#cf-toolsuite/cf-hoover/master-SNAPSHOT) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 4 | 5 | * [Background](docs/BACKGROUND.md) 6 | * [Prerequisites](docs/PREREQUISITES.md) 7 | * [Tools](docs/TOOLS.md) 8 | * How to 9 | * [Clone](docs/CLONING.md) 10 | * [Build](docs/BUILD.md) 11 | * [Manage configuration](docs/CONFIGURATION.md) 12 | * [Run](docs/RUN.md) 13 | * [Check code quality](docs/SONARQUBE.md) 14 | * [Deploy to Tanzu Application Service](docs/TAS.md) 15 | * [Consume endpoints](docs/ENDPOINTS.md) 16 | -------------------------------------------------------------------------------- /docs/BACKGROUND.md: -------------------------------------------------------------------------------- 1 | # VMware Tanzu Application Service > Hoover 2 | 3 | ## Background 4 | 5 | You're already aware of and are using [cf-butler](https://github.com/cf-toolsuite/cf-butler) to help report on and manage application and service instances. Wouldn't it be nice to easily aggregate reporting across multiple foundations? This is `cf-hoover`'s raison d'être. 6 | 7 | ![cf-hoover diagram](cf-hoover-2024.png) -------------------------------------------------------------------------------- /docs/BUILD.md: -------------------------------------------------------------------------------- 1 | # VMware Tanzu Application Service > Hoover 2 | 3 | ## How to Build 4 | 5 | ``` 6 | ./mvnw clean package 7 | ``` 8 | 9 | ### Alternatives 10 | 11 | The below represent a collection of Maven profiles available in the Maven POM. 12 | 13 | * Log4J2 logging (log4j2) 14 | * swaps out [Logback](http://logback.qos.ch/documentation.html) logging provider for [Log4J2](https://logging.apache.org/log4j/2.x/manual/async.html) and [Disruptor](https://lmax-exchange.github.io/disruptor/user-guide/index.html#_introduction) 15 | * Native image (native) 16 | * uses [Spring AOT](https://docs.spring.io/spring-native/docs/current/reference/htmlsingle/#spring-aot-maven) to compile a native executable with [GraalVM](https://www.graalvm.org/docs/introduction/) 17 | 18 | 19 | ``` 20 | ./mvnw clean package -Plog4j2 21 | ``` 22 | > Swap out default "lossy" logging provider 23 | 24 | 25 | ``` 26 | # Using Cloud Native Buildpacks image 27 | ./mvnw spring-boot:build-image -Pnative 28 | 29 | # Using pre-installed Graal CE 30 | ./mvnw native:compile -Pnative -DskipTests 31 | ``` 32 | -------------------------------------------------------------------------------- /docs/CLONING.md: -------------------------------------------------------------------------------- 1 | # VMware Tanzu Application Service > Hoover 2 | 3 | ## Clone 4 | 5 | ``` 6 | git clone https://github.com/cf-toolsuite/cf-hoover.git 7 | ``` 8 | -------------------------------------------------------------------------------- /docs/CONFIGURATION.md: -------------------------------------------------------------------------------- 1 | # VMware Tanzu Application Service > Hoover 2 | 3 | ## How to configure 4 | 5 | Make a copy of then edit the contents of the `application.yml` file located in `src/main/resources`. A best practice is to append a suffix representing the target deployment environment (e.g., `application-pws.yml`, `application-pcfone.yml`). You will need to provide administrator credentials to Apps Manager for the foundation if you want the butler to keep your entire foundation tidy. 6 | 7 | > You really should not bundle configuration with the application. To take some of the sting away, you might consider externalizing and/or [encrypting](https://docs.spring.io/spring-cloud-config/docs/current/reference/html/#_encryption_and_decryption) this configuration. 8 | 9 | ### Managing external configuration 10 | 11 | Create a [Git](https://git-scm.com/docs/gittutorial) repository or work with a [Vault](https://www.baeldung.com/vault) instance as the home your configuration. Cf-hoover has a dependency on the [Spring Cloud Config](https://cloud.spring.io/spring-cloud-static/spring-cloud-config/2.1.0.RELEASE/single/spring-cloud-config.html#_locating_remote_configuration_resources) client, but is disabled by default. 12 | 13 | The `cloud` [profile](https://spring.io/understanding/profiles) enables the client, so when you [cf push](https://docs.run.pivotal.io/devguide/deploy-apps/deploy-app.html#push) cf-hoover, [bind](https://cli.cloudfoundry.org/en-US/cf/bind-service.html) it to a [properly configured](https://docs.pivotal.io/spring-cloud-services/2-0/common/config-server/configuring-with-git.html#general-configuration) Config Server [service instance](https://docs.pivotal.io/spring-cloud-services/2-0/common/config-server/managing-service-instances.html), and start the app instance, it will consult the [Git](https://docs.pivotal.io/spring-cloud-services/2-0/common/config-server/configuration-properties.html#git-global-configuration) repo or [Vault](https://docs.pivotal.io/spring-cloud-services/2-0/common/config-server/configuration-properties.html#vault-global-configuration) instance for configuration to target and aggregate results from one or more previously deployed [cf-butler](https://github.com/cf-toolsuite/cf-butler) instances. 14 | 15 | A sample repository exists for your perusal [here](https://github.com/cf-toolsuite/cf-hoover-config). 16 | 17 | ### Minimum required keys 18 | 19 | At a minimum you should supply values for the `cf.butlers` map of butler routes via one of the following methods 20 | 21 | #### Properties 22 | ``` 23 | cf.butlers.pws=cf-butler-grateful-mouse.cfapps.io 24 | cf.butlers.pcfone=cf-butler-active-tasmaniandevil.apps.pcfone.io 25 | ``` 26 | 27 | #### application.yml 28 | ```yaml 29 | cf: 30 | butlers: 31 | pws: cf-butler-grateful-mouse.cfapps.io 32 | pcfone: cf-butler-active-tasmaniandevil.apps.pcfone.io 33 | ``` 34 | Each key is an alias for a foundation and each value is the route to an application instance of cf-butler deployed on that foundation. If you don't include a protocol, it'll default to `https`. 35 | 36 | ### General configuration notes 37 | 38 | If you copied and appended a suffix to the original `application.yml` then you would set `spring-boot.run.profiles` to be that suffix. For example if you had a configuration file named `application-pws.yml`: 39 | 40 | ``` 41 | ./mvnw spring-boot:run -Dspring-boot.run.profiles=pws 42 | ``` 43 | 44 | Consult the [samples](../samples) directory for additional examples. 45 | -------------------------------------------------------------------------------- /docs/ENDPOINTS.md: -------------------------------------------------------------------------------- 1 | # VMware Tanzu Application Service > Hoover 2 | 3 | * [Endpoints](#endpoints) 4 | * [Snapshot](#snapshot) 5 | * [Organizations, spaces, users](#organizations-spaces-users) 6 | * [Summary](#summary) 7 | * [Detail](#detail) 8 | * [Demographics](#demographics) 9 | * [Java Applications](#java-applications) 10 | * [Collection](#collection) 11 | * [Accounting](#accounting) 12 | 13 | Note: _Accounting_ endpoints are only available when the target foundation hosts Tanzu Application Service. 14 | 15 | ## Endpoints 16 | 17 | These REST endpoints have been exposed for administrative purposes. 18 | 19 | ### Snapshot 20 | 21 | #### Organizations, spaces, users 22 | 23 | ``` 24 | GET /snapshot/organizations 25 | ``` 26 | > Assembles list of organizations per foundation registered 27 | 28 | ``` 29 | GET /snapshot/spaces 30 | ``` 31 | > Assembles list of spaces per foundation registered 32 | 33 | 34 | ``` 35 | GET /snapshot/spaces/users 36 | ``` 37 | > Provides details and light metrics for users by role within all organizations and spaces across all registered foundations 38 | 39 | Sample output 40 | ``` 41 | [ 42 | { 43 | foundation: "pws" 44 | organization: "Northwest", 45 | space: "akarode", 46 | auditors: [ ], 47 | developers: [ 48 | "wlund@pivotal.io", 49 | "akarode@pivotal.io" 50 | ], 51 | managers: [ 52 | "wlund@pivotal.io", 53 | "akarode@pivotal.io" 54 | ], 55 | users: [ 56 | "wlund@pivotal.io", 57 | "akarode@pivotal.io" 58 | ], 59 | user-count: 2, 60 | }, 61 | { 62 | foundation: "pws" 63 | organization: "Northwest", 64 | space: "arao", 65 | auditors: [ ], 66 | developers: [ 67 | "arao@pivotal.io" 68 | ], 69 | managers: [ 70 | "arao@pivotal.io" 71 | ], 72 | users: [ 73 | "arao@pivotal.io" 74 | ], 75 | user-count: 1 76 | }, 77 | ... 78 | ``` 79 | > `users` is the unique subset of all users from each role in the organization/space 80 | 81 | ``` 82 | GET /snapshot/{foundation}/{organization}/{space}/users 83 | ``` 84 | > Provides details and light metrics for users by role within a targeted foundation (alias), organization and space 85 | 86 | ``` 87 | GET /snapshot/users 88 | ``` 89 | > Lists all unique user accounts across all registered foundations 90 | 91 | ``` 92 | GET /snapshot/users/count 93 | ``` 94 | > Counts the number of user accounts across all registered foundations 95 | 96 | #### Summary 97 | 98 | ``` 99 | GET /snapshot/summary 100 | ``` 101 | > Provides summary metrics for applications and service instances across all registered foundations 102 | 103 | > **Note**: this summary report does not take the place of an official foundation Accounting Report. The Accounting Report is focussed on calculating aggregates (on a monthly basis) such as: (a) the total hours of application instance usage, (b) the largest # of application instances running (a.k.a. maximum concurrent application instances), c) the total hours of service instance usage and (d) the largest # of service instances running (a.k.a. maximum concurrent service instances). 104 | 105 | Sample output 106 | ``` 107 | { 108 | "application-counts": { 109 | "by-buildpack": { 110 | "java": 28, 111 | "nodejs": 2, 112 | "unknown": 5 113 | }, 114 | "by-stack": { 115 | "cflinuxfs2": 20, 116 | "cflinuxfs3": 15 117 | }, 118 | "by-dockerimage": { 119 | "--": 0 120 | }, 121 | "by-status": { 122 | "stopped": 15, 123 | "started": 20 124 | }, 125 | "total-applications": 35, 126 | "total-running-application-instances": 21, 127 | "total-stopped-application-instances": 18, 128 | "total-crashed-application-instances": 3, 129 | "total-application-instances": 42, 130 | "velocity": { 131 | "between-two-days-and-one-week": 6, 132 | "between-one-week-and-two-weeks": 0, 133 | "between-one-day-and-two-days": 3, 134 | "between-one-month-and-three-months": 5, 135 | "between-three-months-and-six-months": 4, 136 | "between-two-weeks-and-one-month": 1, 137 | "in-last-day": 0, 138 | "between-six-months-and-one-year": 10, 139 | "beyond-one-year": 6 140 | } 141 | }, 142 | "service-instance-counts": { 143 | "by-organization": { 144 | "Northwest": 37 145 | }, 146 | "by-service": { 147 | "rediscloud": 2, 148 | "elephantsql": 4, 149 | "mlab": 2, 150 | "p-service-registry": 2, 151 | "cleardb": 10, 152 | "p-config-server": 2, 153 | "user-provided": 9, 154 | "app-autoscaler": 2, 155 | "cloudamqp": 4 156 | }, 157 | "by-service-and-plan": { 158 | "cleardb/spark": 10, 159 | "mlab/sandbox": 2, 160 | "rediscloud/30mb": 2, 161 | "p-service-registry/trial": 2, 162 | "elephantsql/turtle": 4, 163 | "p-config-server/trial": 2, 164 | "cloudamqp/lemur": 4, 165 | "app-autoscaler/standard": 2 166 | }, 167 | "total-service-instances": 37, 168 | "velocity": { 169 | "between-two-days-and-one-week": 4, 170 | "between-one-week-and-two-weeks": 1, 171 | "between-one-day-and-two-days": 2, 172 | "between-one-month-and-three-months": 3, 173 | "between-three-months-and-six-months": 0, 174 | "between-two-weeks-and-one-month": 1, 175 | "in-last-day": 0, 176 | "between-six-months-and-one-year": 5, 177 | "beyond-one-year": 8 178 | } 179 | } 180 | } 181 | ``` 182 | 183 | #### Detail 184 | 185 | ``` 186 | GET /snapshot/detail 187 | ``` 188 | > Provides lists of all applications and service instances (by foundation, organization and space) and accounts (split into sets of user and service names) 189 | 190 | > **Note**: this detail report does not take the place of an official foundation Accounting Report. However, it does provide a much more detailed snapshot of all the applications that were currently running at the time of collection. 191 | 192 | ``` 193 | GET /snapshot/detail/ai 194 | ``` 195 | > Provides lists of all applications in comma-separated value format 196 | 197 | Sample output 198 | 199 | ``` 200 | foundation,organization,space,application id,application name,buildpack,buildpack version,image,stack,running instances,total instances,memory used (in gb),memory quota (in gb),disk used (in gb),disk quota (in gb),urls,last pushed,last event,last event actor,last event time,requested state 201 | "npike-foundation","arul","dev","ff1f2147-079c-4f58-bbe7-ad0fb905a2e8","pcfdemo",,,,"cflinuxfs3","0","1","0.0","0.0","pcfdemo.apps.sangabriel.cf-app.com",,"audit.app.update","bcbc230c-2ecc-4dc9-91de-6b49776ad403","2023-05-12T16:10:12","stopped" 202 | "npike-foundation","arul","dev","655aab3a-8b77-42ce-a87b-aa5848cc9d7d","rabbitmq-example-app",,,,"cflinuxfs3","0","2","0.0","0.0","rabbitmq-example-app.apps.sangabriel.cf-app.com","2023-05-12T16:10:14","audit.app.build.create","bcbc230c-2ecc-4dc9-91de-6b49776ad403","2023-05-12T16:10:27","stopped" 203 | "npike-foundation","arul","dev","b814712d-03e9-44b2-ac28-1946cbdbc82c","spring-music","java","v4.54",,"cflinuxfs3","1","1","0.21608664747327566","0.16757965087890625","spring-music-noisy-kookaburra-eg.apps.sangabriel.cf-app.com","2023-05-12T16:10:14","audit.app.restart","bcbc230c-2ecc-4dc9-91de-6b49776ad403","2023-05-12T16:11:28","started" 204 | "npike-foundation","arul","prod","3a414a89-388c-4625-9fbf-0cd2b889345a","rabbitmq",,,,"cflinuxfs3","0","2","0.0","0.0","rabbitmq.apps.sangabriel.cf-app.com","2023-05-12T16:10:14","audit.app.build.create","01ecf2c7-f4dd-4ca5-8dfd-30df64f09918","2023-05-12T16:10:25","stopped" 205 | "npike-foundation","credhub-service-broker-org","credhub-service-broker-space","b95adbde-2597-4150-b048-8be45796cab6","credhub-broker-1.5.1","binary","1.1.3",,"cflinuxfs3","1","1","0.016004773788154125","0.009128570556640625","credhub-broker.apps.sangabriel.cf-app.com","2023-05-12T15:31:52","audit.app.droplet.create","82dcc4bb-ef83-4db8-b05a-a0e2b88e67e3","2023-05-12T15:32:21","started" 206 | "npike-foundation","dev","observability","be785d20-fd7e-4674-bb7f-8b742b40e1d4","cf-butler","java","v4.54",,"cflinuxfs3","1","1","0.1790512539446354","0.211700439453125","cf-butler.apps.sangabriel.cf-app.com","2023-06-07T00:54:18","audit.app.restart","bcbc230c-2ecc-4dc9-91de-6b49776ad403","2023-06-07T00:55:07","started" 207 | "npike-foundation","dev","observability","2858eaba-5c18-4916-849b-7676c6bb33c5","cf-hoover","java","v4.54",,"cflinuxfs3","1","1","0.40730543807148933","0.2159271240234375","cf-hoover-forgiving-wombat-sr.apps.sangabriel.cf-app.com","2023-04-14T12:54:07",,,,"started" 208 | "npike-foundation","dev","observability","f1e7eb6c-7d54-4968-b6e4-3f9f8fade059","cf-hoover-ui","java","v4.54",,"cflinuxfs3","1","1","0.5119553785771132","0.2413177490234375","cf-hoover-ui-chatty-chimpanzee-jf.apps.sangabriel.cf-app.com","2023-04-14T14:30:03",,,,"started" 209 | "npike-foundation","dev","sample-apps","89b4ae55-705a-4572-9890-46d5171a613c","nicky-butler","java","v4.54",,"cflinuxfs3","0","1","0.0","0.0","nicky-butler.apps.sangabriel.cf-app.com","2023-04-14T18:44:03","audit.app.stop","bcbc230c-2ecc-4dc9-91de-6b49776ad403","2023-05-16T02:45:57","stopped" 210 | "npike-foundation","dev","tap","1fdc8b02-99a7-4741-8c9d-7df4363a7f4d","tas-java-web-app",,,"dev.registry.pivotal.io/warroyo/supply-chain/tas-java-web-app-dev-tap@sha256:0943939b3ca1bcb6527ad39bf766d84b9be4455a14a0b67ab7c4b6f4840750e1","cflinuxfs3","2","2","0.352079289034009","0.5242393091320992","tas-java-web-app.apps.sangabriel.cf-app.com","2023-05-10T18:36:44","audit.app.update","9602fa7e-d66f-4a0e-b78c-b191106b79c4","2023-05-10T18:40:46","started" 211 | "npike-foundation","p-spring-cloud-services","249f77f1-63a9-41c2-a26a-ad848df3fcba","3e4c55ca-ed5f-4e97-86c2-25d38626b90d","config-server","java","v4.54",,"cflinuxfs3","1","1","0.24609375","0.15219497680664062","config-server-249f77f1-63a9-41c2-a26a-ad848df3fcba.apps.sangabriel.cf-app.com","2023-04-14T12:54:45",,,,"started" 212 | "npike-foundation","p-spring-cloud-services","63df5b0a-b4a8-4be1-8aad-c54dab4cb7ed","893f0f47-6eed-4bd0-bb83-5472b7ea4c07","service-registry","java","v4.54",,"cflinuxfs3","1","1","0.2760823564603925","0.17532730102539062","service-registry-63df5b0a-b4a8-4be1-8aad-c54dab4cb7ed.apps.sangabriel.cf-app.com","2023-04-14T12:54:47",,,,"started" 213 | "npike-foundation","system","autoscaling","d7396dab-0a12-4e68-a78f-c6008d5051a7","autoscale","binary","1.1.3",,"cflinuxfs3","3","3","0.0546162910759449","0.052013397216796875","autoscale.sys.sangabriel.cf-app.com","2023-04-13T15:06:22",,,,"started" 214 | "npike-foundation","system","autoscaling","c5aa1354-6dd4-41f5-aa7b-939a19029532","autoscale-api","java","v4.54",,"cflinuxfs3","1","1","0.2501183710992336","0.188018798828125","autoscale.sys.sangabriel.cf-app.com/api/v2","2023-04-13T15:06:51",,,,"started" 215 | "npike-foundation","system","notifications-with-ui","806444bb-3da7-468e-9da6-d5bde5b56fd7","notifications-ui","binary","1.1.3",,"cflinuxfs3","2","2","0.021087645553052425","0.02587890625","notifications-ui.sys.sangabriel.cf-app.com","2023-04-13T15:05:45",,,,"started" 216 | "npike-foundation","system","offline-docs","3b30b7d8-d170-41c7-b4e3-20a0e4eb369a","offline-docs","ruby","1.9.2",,"cflinuxfs3","1","1","0.09670342318713665","0.21299362182617188","offline-docs.apps.sangabriel.cf-app.com","2023-04-13T15:01:02",,,,"started" 217 | "npike-foundation","system","p-dataflow","ed513df1-a946-45b8-8975-2db788f0bbec","p-dataflow-1.13.0","java","v4.54",,"cflinuxfs3","1","1","0.6273137014359236","0.44135284423828125","p-dataflow.apps.sangabriel.cf-app.com","2023-04-27T18:13:46",,,,"started" 218 | "npike-foundation","system","system","ecd6f8da-38ef-4714-9d84-cee2cb5f13d4","app-usage-scheduler","ruby","1.9.2",,"cflinuxfs3","1","1","0.0999792842194438","0.1661376953125",,"2023-04-13T14:56:21",,,,"started" 219 | "npike-foundation","system","system","9bd4378d-c97f-46cd-bcf9-e99fcd56297d","app-usage-server","ruby","1.9.2",,"cflinuxfs3","2","2","0.5762754492461681","0.33228302001953125","app-usage.sys.sangabriel.cf-app.com","2023-04-13T14:56:21",,,,"started" 220 | "npike-foundation","system","system","fefc5be6-90ee-4b87-8815-1c13af429dd9","app-usage-worker","ruby","1.9.2",,"cflinuxfs3","1","1","0.11408127937465906","0.1661376953125",,"2023-04-13T14:56:20",,,,"started" 221 | "npike-foundation","system","system","31c7e146-0949-40a4-b57f-d11d403916e1","apps-manager-js-green","staticfile","1.6.0",,"cflinuxfs3","6","6","0.10966186318546534","0.78497314453125","apps.sys.sangabriel.cf-app.com","2023-04-13T15:02:38",,,,"started" 222 | "npike-foundation","system","system","b610eb6f-9c9f-4906-872d-d12021232755","p-invitations-green","nodejs","1.8.6",,"cflinuxfs3","2","2","0.12076483760029078","0.36865997314453125","p-invitations.sys.sangabriel.cf-app.com","2023-04-13T15:02:32",,,,"started" 223 | "npike-foundation","system","system","586a6e4e-b6df-4fe5-88a3-eb9b7c667144","search-server-green","nodejs","1.8.6",,"cflinuxfs3","2","2","0.1306796595454216","0.3582916259765625","search-server.sys.sangabriel.cf-app.com","2023-04-13T15:02:29",,,,"started" 224 | "npike-foundation","zoo-labs","demo","35eb5fdf-46a3-4d77-bfe6-dd29878a1553","primes","java","v4.54",,"cflinuxfs4","2","2","0.3480446543544531","0.35300445556640625","primes-bogus-hedgehog-kx.apps.sangabriel.cf-app.com","2023-05-25T17:59:41","audit.app.environment.show","bcbc230c-2ecc-4dc9-91de-6b49776ad403","2023-06-06T20:15:19","started" 225 | ``` 226 | 227 | ``` 228 | GET /snapshot/detail/si 229 | ``` 230 | > Provides lists of all service instances in comma-separated value format 231 | 232 | Sample output 233 | 234 | ``` 235 | foundation,organization,space,service instance id,name,service,description,plan,type,bound applications,last operation,last updated,dashboard url,requested state 236 | "npike-foundation","dev","observability","efe26b17-0722-4b9d-8160-2b804adf4bbc","cf-butler-secrets",,,,"user_provided_service_instance","cf-butler",,,, 237 | "npike-foundation","dev","observability","249f77f1-63a9-41c2-a26a-ad848df3fcba","cf-hoover-config","p.config-server","Service to provide configuration to applications at runtime.","standard","managed_service_instance","cf-hoover","create","2023-04-14T12:56:27","https://config-server-249f77f1-63a9-41c2-a26a-ad848df3fcba.apps.sangabriel.cf-app.com/dashboard","succeeded" 238 | "npike-foundation","dev","observability","731a52c3-f189-47cb-b936-0442ad63da26","cf-butler-backend","p.mysql","Dedicated instances of MySQL","db-small-80","managed_service_instance","cf-butler","create","2023-04-13T20:53:47",,"succeeded" 239 | "npike-foundation","dev","observability","63df5b0a-b4a8-4be1-8aad-c54dab4cb7ed","hooverRegistry","p.service-registry","Deploys Eureka server as a service registry for application clients.","standard","managed_service_instance","cf-hoover,cf-hoover-ui","create","2023-04-14T12:56:33","https://service-registry-63df5b0a-b4a8-4be1-8aad-c54dab4cb7ed.apps.sangabriel.cf-app.com/dashboard","succeeded" 240 | "npike-foundation","dev","sample-apps","69ee91b3-7493-45bd-b0bb-d9dc3e04a0bd","cf-butler-backend","p.mysql","Dedicated instances of MySQL","db-small-80","managed_service_instance","nicky-butler","create","2023-04-14T15:20:18",,"succeeded" 241 | "npike-foundation","p-spring-cloud-services","249f77f1-63a9-41c2-a26a-ad848df3fcba","5f18839e-8425-4e58-925b-4f4cab17a378","mirror-svc","p.mirror-service","Spring Cloud Config Server git mirror service. This is an internal system service and should not be created directly by end users.","standard","managed_service_instance","config-server","create","2023-04-14T12:54:31",,"succeeded" 242 | "npike-foundation","system","system","c004a59c-c604-430a-b19a-345f1f3653c2","structured-format-json",,,,"user_provided_service_instance","app-usage-worker",,,, 243 | ``` 244 | 245 | #### Demographics 246 | 247 | ``` 248 | GET /snapshot/demographics 249 | ``` 250 | > Yields organization, space, user account, and service account totals across all registered foundations 251 | 252 | Sample output 253 | 254 | ``` 255 | { 256 | "demographics": [ 257 | { 258 | "foundation": "npike-foundation", 259 | "total-organizations": 7, 260 | "total-service-accounts": 5, 261 | "total-spaces": 18, 262 | "total-user-accounts": 0 263 | } 264 | ], 265 | "total-foundations": 1, 266 | "total-service-accounts": 5, 267 | "total-user-accounts": 0 268 | } 269 | ``` 270 | 271 | #### Java Applications 272 | 273 | ``` 274 | GET /snapshot/detail/ai/spring 275 | ``` 276 | > Returns a filtered list of applications that are utilizing Spring dependencies across all foundations 277 | 278 | Sample output 279 | 280 | ``` 281 | [ 282 | { 283 | "foundation" : "isle", 284 | "appId": "f381a7dd-42df-4c57-9d30-37f8ade12012", 285 | "appName": "cf-butler", 286 | "dropletId": "797b9bfd-0de2-48a9-b22e-90d7a61fd988", 287 | "organization": "observability", 288 | "space": "demo", 289 | "springDependencies": "org.springframework.boot:spring-boot-starter-parent:3.2.2, org.springframework.cloud:spring-cloud-dependencies:2023.0.1" 290 | } 291 | ] 292 | ``` 293 | 294 | ``` 295 | GET /snapshot/summary/ai/spring 296 | ``` 297 | > Calculates the frequency of occurrence for each Spring dependency found across all registered foundations 298 | 299 | Sample output 300 | 301 | ``` 302 | { 303 | "org.springframework.boot:spring-boot-starter-parent:3.2.2": 10, 304 | "org.springframework.cloud:spring-cloud-dependencies:2023.0.1": 7 305 | } 306 | ``` 307 | 308 | 309 | ### Collection 310 | 311 | ``` 312 | GET /collect 313 | ``` 314 | > Returns time keeping records (i.e., the date/time of collection of snapshot data from each cf-butler instance registered with cf-hoover) 315 | 316 | Sample output 317 | 318 | ``` 319 | { 320 | "time-keepers": [ 321 | { 322 | "collectionDateTime": "2023-06-20T03:13:29", 323 | "foundation": "npike-foundation" 324 | } 325 | ] 326 | } 327 | ``` 328 | 329 | ### Accounting 330 | 331 | ``` 332 | GET /accounting/applications 333 | ``` 334 | > Produces an aggregate system-wide account report of [application usage](https://docs.vmware.com/en/VMware-Tanzu-Application-Service/4.0/tas-for-vms/accounting-report.html#app-usage) 335 | 336 | > **Note**: Report excludes application instances in the `system` org 337 | 338 | ``` 339 | GET /accounting/services 340 | ``` 341 | > Produces an aggregate system-wide account report of [service usage](https://docs.vmware.com/en/VMware-Tanzu-Application-Service/4.0/tas-for-vms/accounting-report.html#service-usage) 342 | 343 | > **Note**: Report excludes user-provided service instances 344 | 345 | ``` 346 | GET /accounting/tasks 347 | ``` 348 | > Produces an aggregate system-wide account report of [task usage](https://docs.vmware.com/en/VMware-Tanzu-Application-Service/4.0/tas-for-vms/accounting-report.html#task-usage) 349 | -------------------------------------------------------------------------------- /docs/PREREQUISITES.md: -------------------------------------------------------------------------------- 1 | # VMware Tanzu Application Service > Hoover 2 | 3 | ## Prerequisites 4 | 5 | Required 6 | 7 | * Access to a [foundation with Cloud Foundry](https://docs.cloudfoundry.org/deploying/cf-deployment/index.html) that was deployed with [cf-deployment](https://github.com/cloudfoundry/cf-deployment/releases) v30.10.0 or better 8 | * You will need to deploy the open source versions of Spring Cloud [Config Server](https://docs.spring.io/spring-cloud-config/docs/current/reference/html/) and [Discovery Service](https://docs.spring.io/spring-cloud/docs/current/reference/htmlsingle/#spring-cloud-running-eureka-server). Review the examples [here](https://github.com/cf-toolsuite/home/tree/main/footprints/local/support/config-server) and [here](https://github.com/cf-toolsuite/home/tree/main/footprints/local/support/discovery-service). 9 | 10 | or 11 | 12 | * Access to a [foundation with VMware Tanzu Application Service](https://pivotal.io/platform/pivotal-application-service) 4.0.19+LTS-T or better installed 13 | * [Spring Cloud Services, Config Server](https://docs.vmware.com/en/Spring-Cloud-Services-for-VMware-Tanzu/3.1/spring-cloud-services/GUID-config-server-configuring-with-git.html) 3.1.x or better installed 14 | 15 | Optional 16 | 17 | * [Spring Cloud Services, Service Registry](https://docs.vmware.com/en/Spring-Cloud-Services-for-VMware-Tanzu/3.1/spring-cloud-services/GUID-service-registry-index.html) 3.1.x or better installed 18 | -------------------------------------------------------------------------------- /docs/RUN.md: -------------------------------------------------------------------------------- 1 | # VMware Tanzu Application Service > Hoover 2 | 3 | ## How to Run with Maven 4 | 5 | ```shell 6 | ./mvnw spring-boot:run -Dspring-boot.run.profiles={target_foundation_profile} 7 | ``` 8 | where `{target_foundation_profile}` is something like `dev`, `pws` or `pcfone`. 9 | 10 | You'll need to manually stop to the application with `Ctrl+C` 11 | 12 | ### Running with Hoover-UI 13 | If you intend to optionally setup [cf-hoover-ui](https://github.com/cf-toolsuite/cf-hoover-ui) for a local development, then you must first launch a standalone instance of [Eureka server](https://cloud.spring.io/spring-cloud-netflix/multi/multi_spring-cloud-eureka-server.html) for the hoover-ui to be able to find the hoover server. 14 | 15 | Enable cloud discovery before launching `cf-hoover` via the command line or application config: 16 | 17 | ``` 18 | ./mvnw spring-boot:run -Dspring-boot.run.profiles=pws -Dspring.cloud.discovery.enabled=true 19 | ``` 20 | 21 | or via application.yml 22 | ```yaml 23 | spring: 24 | discovery: 25 | enabled: true 26 | ``` 27 | -------------------------------------------------------------------------------- /docs/SONARQUBE.md: -------------------------------------------------------------------------------- 1 | # VMware Tanzu Application Service > Hoover 2 | 3 | ## How to check code quality with Sonarqube 4 | 5 | Launch an instance of Sonarqube on your workstation with Docker 6 | 7 | ``` 8 | docker run -d --name sonarqube -p 9000:9000 -p 9092:9092 sonarqube 9 | ``` 10 | 11 | Then make sure to add goal and required arguments when building with Maven. For example: 12 | 13 | ``` 14 | mvn sonar:sonar -Dsonar.login=admin -Dsonar.password=admin 15 | ``` 16 | 17 | Then visit `http://localhost:9000` in your favorite browser to inspect results of scan. 18 | -------------------------------------------------------------------------------- /docs/TAS.md: -------------------------------------------------------------------------------- 1 | # VMware Tanzu Application Service > Hoover 2 | 3 | ## How to deploy to VMware Tanzu Application Service 4 | 5 | Please review the [manifest.yml](../manifest.yml) before deploying. 6 | 7 | ### Using scripts 8 | 9 | Deploy the app (bound to an instance of Spring Cloud Config Server) 10 | 11 | Create a file named `config-server.json` located in a `config` sub-directory off the root of this project. Look at the sample [here](../samples/config-server.json) to get an idea of the contents. Consult the Spring Cloud Services Config Server [documentation](https://docs.pivotal.io/spring-cloud-services/2-0/common/config-server/index.html) for more advanced configuration options like [SSH repository access](https://docs.pivotal.io/spring-cloud-services/2-0/common/config-server/configuring-with-git.html#ssh-repository-access). 12 | 13 | Then execute 14 | 15 | ``` 16 | ./scripts/deploy.sh 17 | ``` 18 | 19 | Shutdown and destroy the app and service instances with 20 | 21 | ``` 22 | ./scripts/destroy.sh 23 | ``` 24 | 25 | > Note: If you are seeing [OutOfMemory exceptions](https://dzone.com/articles/troubleshooting-problems-with-native-off-heap-memo) shortly after startup you may need to [cf scale](https://docs.pivotal.io/application-service/2-10/devguide/deploy-apps/cf-scale.html#verticall) the available memory when working with multiple foundations. 26 | 27 | Alternative scripts exist for deploying `cf-hoover` when working with `cf-hoover-ui`. See [deploy.with-registry.sh](../scripts/deploy.with-registry.sh) and [destroy.with-registry.sh](../scripts/destroy.with-registry.sh). -------------------------------------------------------------------------------- /docs/TOOLS.md: -------------------------------------------------------------------------------- 1 | # VMware Tanzu Application Service > Hoover 2 | 3 | ## Tools 4 | 5 | * [git](https://git-scm.com/downloads) 2.40.0 or better 6 | * [JDK](http://openjdk.java.net/install/) 21 or better 7 | * [cf](https://docs.cloudfoundry.org/cf-cli/install-go-cli.html) CLI 8.6.1 or better 8 | -------------------------------------------------------------------------------- /docs/cf-hoover-2024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cf-toolsuite/cf-hoover/0a069938fe1cfb50db686b699602e7264f476c33/docs/cf-hoover-2024.png -------------------------------------------------------------------------------- /manifest.with-registry.yml: -------------------------------------------------------------------------------- 1 | --- 2 | applications: 3 | - name: cf-hoover 4 | memory: 2G 5 | stack: cflinuxfs4 6 | path: target/cf-hoover-1.0-SNAPSHOT.jar 7 | instances: 1 8 | env: 9 | JAVA_OPTS: -XX:MaxDirectMemorySize=512m -Djava.security.egd=file:///dev/urandom 10 | SPRING_CLOUD_DISCOVERY_ENABLED: true 11 | SPRING_PROFILES_ACTIVE: secrets,cloud 12 | JBP_CONFIG_OPEN_JDK_JRE: '{ jre: { version: 21.+ } }' 13 | -------------------------------------------------------------------------------- /manifest.yml: -------------------------------------------------------------------------------- 1 | --- 2 | applications: 3 | - name: cf-hoover 4 | memory: 2G 5 | stack: cflinuxfs4 6 | path: target/cf-hoover-1.0-SNAPSHOT.jar 7 | instances: 1 8 | env: 9 | JAVA_OPTS: -XX:MaxDirectMemorySize=512m -Djava.security.egd=file:///dev/urandom 10 | SPRING_PROFILES_ACTIVE: secrets,cloud 11 | JBP_CONFIG_OPEN_JDK_JRE: '{ jre: { version: 21.+ } }' 12 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. 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, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Apache Maven Wrapper startup batch script, version 3.3.2 23 | # 24 | # Optional ENV vars 25 | # ----------------- 26 | # JAVA_HOME - location of a JDK home dir, required when download maven via java source 27 | # MVNW_REPOURL - repo url base for downloading maven distribution 28 | # MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 29 | # MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output 30 | # ---------------------------------------------------------------------------- 31 | 32 | set -euf 33 | [ "${MVNW_VERBOSE-}" != debug ] || set -x 34 | 35 | # OS specific support. 36 | native_path() { printf %s\\n "$1"; } 37 | case "$(uname)" in 38 | CYGWIN* | MINGW*) 39 | [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" 40 | native_path() { cygpath --path --windows "$1"; } 41 | ;; 42 | esac 43 | 44 | # set JAVACMD and JAVACCMD 45 | set_java_home() { 46 | # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched 47 | if [ -n "${JAVA_HOME-}" ]; then 48 | if [ -x "$JAVA_HOME/jre/sh/java" ]; then 49 | # IBM's JDK on AIX uses strange locations for the executables 50 | JAVACMD="$JAVA_HOME/jre/sh/java" 51 | JAVACCMD="$JAVA_HOME/jre/sh/javac" 52 | else 53 | JAVACMD="$JAVA_HOME/bin/java" 54 | JAVACCMD="$JAVA_HOME/bin/javac" 55 | 56 | if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then 57 | echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 58 | echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 59 | return 1 60 | fi 61 | fi 62 | else 63 | JAVACMD="$( 64 | 'set' +e 65 | 'unset' -f command 2>/dev/null 66 | 'command' -v java 67 | )" || : 68 | JAVACCMD="$( 69 | 'set' +e 70 | 'unset' -f command 2>/dev/null 71 | 'command' -v javac 72 | )" || : 73 | 74 | if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then 75 | echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 76 | return 1 77 | fi 78 | fi 79 | } 80 | 81 | # hash string like Java String::hashCode 82 | hash_string() { 83 | str="${1:-}" h=0 84 | while [ -n "$str" ]; do 85 | char="${str%"${str#?}"}" 86 | h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) 87 | str="${str#?}" 88 | done 89 | printf %x\\n $h 90 | } 91 | 92 | verbose() { :; } 93 | [ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } 94 | 95 | die() { 96 | printf %s\\n "$1" >&2 97 | exit 1 98 | } 99 | 100 | trim() { 101 | # MWRAPPER-139: 102 | # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. 103 | # Needed for removing poorly interpreted newline sequences when running in more 104 | # exotic environments such as mingw bash on Windows. 105 | printf "%s" "${1}" | tr -d '[:space:]' 106 | } 107 | 108 | # parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties 109 | while IFS="=" read -r key value; do 110 | case "${key-}" in 111 | distributionUrl) distributionUrl=$(trim "${value-}") ;; 112 | distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; 113 | esac 114 | done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" 115 | [ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" 116 | 117 | case "${distributionUrl##*/}" in 118 | maven-mvnd-*bin.*) 119 | MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ 120 | case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in 121 | *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; 122 | :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; 123 | :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; 124 | :Linux*x86_64*) distributionPlatform=linux-amd64 ;; 125 | *) 126 | echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 127 | distributionPlatform=linux-amd64 128 | ;; 129 | esac 130 | distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" 131 | ;; 132 | maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; 133 | *) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; 134 | esac 135 | 136 | # apply MVNW_REPOURL and calculate MAVEN_HOME 137 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 138 | [ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" 139 | distributionUrlName="${distributionUrl##*/}" 140 | distributionUrlNameMain="${distributionUrlName%.*}" 141 | distributionUrlNameMain="${distributionUrlNameMain%-bin}" 142 | MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" 143 | MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" 144 | 145 | exec_maven() { 146 | unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : 147 | exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" 148 | } 149 | 150 | if [ -d "$MAVEN_HOME" ]; then 151 | verbose "found existing MAVEN_HOME at $MAVEN_HOME" 152 | exec_maven "$@" 153 | fi 154 | 155 | case "${distributionUrl-}" in 156 | *?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; 157 | *) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; 158 | esac 159 | 160 | # prepare tmp dir 161 | if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then 162 | clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } 163 | trap clean HUP INT TERM EXIT 164 | else 165 | die "cannot create temp dir" 166 | fi 167 | 168 | mkdir -p -- "${MAVEN_HOME%/*}" 169 | 170 | # Download and Install Apache Maven 171 | verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 172 | verbose "Downloading from: $distributionUrl" 173 | verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 174 | 175 | # select .zip or .tar.gz 176 | if ! command -v unzip >/dev/null; then 177 | distributionUrl="${distributionUrl%.zip}.tar.gz" 178 | distributionUrlName="${distributionUrl##*/}" 179 | fi 180 | 181 | # verbose opt 182 | __MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' 183 | [ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v 184 | 185 | # normalize http auth 186 | case "${MVNW_PASSWORD:+has-password}" in 187 | '') MVNW_USERNAME='' MVNW_PASSWORD='' ;; 188 | has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; 189 | esac 190 | 191 | if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then 192 | verbose "Found wget ... using wget" 193 | wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" 194 | elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then 195 | verbose "Found curl ... using curl" 196 | curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" 197 | elif set_java_home; then 198 | verbose "Falling back to use Java to download" 199 | javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" 200 | targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" 201 | cat >"$javaSource" <<-END 202 | public class Downloader extends java.net.Authenticator 203 | { 204 | protected java.net.PasswordAuthentication getPasswordAuthentication() 205 | { 206 | return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); 207 | } 208 | public static void main( String[] args ) throws Exception 209 | { 210 | setDefault( new Downloader() ); 211 | java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); 212 | } 213 | } 214 | END 215 | # For Cygwin/MinGW, switch paths to Windows format before running javac and java 216 | verbose " - Compiling Downloader.java ..." 217 | "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" 218 | verbose " - Running Downloader.java ..." 219 | "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" 220 | fi 221 | 222 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 223 | if [ -n "${distributionSha256Sum-}" ]; then 224 | distributionSha256Result=false 225 | if [ "$MVN_CMD" = mvnd.sh ]; then 226 | echo "Checksum validation is not supported for maven-mvnd." >&2 227 | echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 228 | exit 1 229 | elif command -v sha256sum >/dev/null; then 230 | if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then 231 | distributionSha256Result=true 232 | fi 233 | elif command -v shasum >/dev/null; then 234 | if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then 235 | distributionSha256Result=true 236 | fi 237 | else 238 | echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 239 | echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 240 | exit 1 241 | fi 242 | if [ $distributionSha256Result = false ]; then 243 | echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 244 | echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 245 | exit 1 246 | fi 247 | fi 248 | 249 | # unzip and move 250 | if command -v unzip >/dev/null; then 251 | unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" 252 | else 253 | tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" 254 | fi 255 | printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" 256 | mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" 257 | 258 | clean || : 259 | exec_maven "$@" 260 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | <# : batch portion 2 | @REM ---------------------------------------------------------------------------- 3 | @REM Licensed to the Apache Software Foundation (ASF) under one 4 | @REM or more contributor license agreements. See the NOTICE file 5 | @REM distributed with this work for additional information 6 | @REM regarding copyright ownership. The ASF licenses this file 7 | @REM to you under the Apache License, Version 2.0 (the 8 | @REM "License"); you may not use this file except in compliance 9 | @REM with the License. You may obtain a copy of the License at 10 | @REM 11 | @REM http://www.apache.org/licenses/LICENSE-2.0 12 | @REM 13 | @REM Unless required by applicable law or agreed to in writing, 14 | @REM software distributed under the License is distributed on an 15 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | @REM KIND, either express or implied. See the License for the 17 | @REM specific language governing permissions and limitations 18 | @REM under the License. 19 | @REM ---------------------------------------------------------------------------- 20 | 21 | @REM ---------------------------------------------------------------------------- 22 | @REM Apache Maven Wrapper startup batch script, version 3.3.2 23 | @REM 24 | @REM Optional ENV vars 25 | @REM MVNW_REPOURL - repo url base for downloading maven distribution 26 | @REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 27 | @REM MVNW_VERBOSE - true: enable verbose log; others: silence the output 28 | @REM ---------------------------------------------------------------------------- 29 | 30 | @IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) 31 | @SET __MVNW_CMD__= 32 | @SET __MVNW_ERROR__= 33 | @SET __MVNW_PSMODULEP_SAVE=%PSModulePath% 34 | @SET PSModulePath= 35 | @FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( 36 | IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) 37 | ) 38 | @SET PSModulePath=%__MVNW_PSMODULEP_SAVE% 39 | @SET __MVNW_PSMODULEP_SAVE= 40 | @SET __MVNW_ARG0_NAME__= 41 | @SET MVNW_USERNAME= 42 | @SET MVNW_PASSWORD= 43 | @IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) 44 | @echo Cannot start maven from wrapper >&2 && exit /b 1 45 | @GOTO :EOF 46 | : end batch / begin powershell #> 47 | 48 | $ErrorActionPreference = "Stop" 49 | if ($env:MVNW_VERBOSE -eq "true") { 50 | $VerbosePreference = "Continue" 51 | } 52 | 53 | # calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties 54 | $distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl 55 | if (!$distributionUrl) { 56 | Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" 57 | } 58 | 59 | switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { 60 | "maven-mvnd-*" { 61 | $USE_MVND = $true 62 | $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" 63 | $MVN_CMD = "mvnd.cmd" 64 | break 65 | } 66 | default { 67 | $USE_MVND = $false 68 | $MVN_CMD = $script -replace '^mvnw','mvn' 69 | break 70 | } 71 | } 72 | 73 | # apply MVNW_REPOURL and calculate MAVEN_HOME 74 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 75 | if ($env:MVNW_REPOURL) { 76 | $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } 77 | $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" 78 | } 79 | $distributionUrlName = $distributionUrl -replace '^.*/','' 80 | $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' 81 | $MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" 82 | if ($env:MAVEN_USER_HOME) { 83 | $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" 84 | } 85 | $MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' 86 | $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" 87 | 88 | if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { 89 | Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" 90 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 91 | exit $? 92 | } 93 | 94 | if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { 95 | Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" 96 | } 97 | 98 | # prepare tmp dir 99 | $TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile 100 | $TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" 101 | $TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null 102 | trap { 103 | if ($TMP_DOWNLOAD_DIR.Exists) { 104 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 105 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 106 | } 107 | } 108 | 109 | New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null 110 | 111 | # Download and Install Apache Maven 112 | Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 113 | Write-Verbose "Downloading from: $distributionUrl" 114 | Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 115 | 116 | $webclient = New-Object System.Net.WebClient 117 | if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { 118 | $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) 119 | } 120 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 121 | $webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null 122 | 123 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 124 | $distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum 125 | if ($distributionSha256Sum) { 126 | if ($USE_MVND) { 127 | Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." 128 | } 129 | Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash 130 | if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { 131 | Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." 132 | } 133 | } 134 | 135 | # unzip and move 136 | Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null 137 | Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null 138 | try { 139 | Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null 140 | } catch { 141 | if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { 142 | Write-Error "fail to move MAVEN_HOME" 143 | } 144 | } finally { 145 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 146 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 147 | } 148 | 149 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 150 | -------------------------------------------------------------------------------- /samples/application-pcfone.yml: -------------------------------------------------------------------------------- 1 | cf: 2 | butlers: 3 | pcfone: cf-butler-appropriate-eagle.apps.pcfone.io 4 | 5 | management: 6 | endpoints: 7 | web: 8 | exposure: 9 | include: info,health,metrics,scheduledtasks,loggers,logfile,prometheus 10 | -------------------------------------------------------------------------------- /samples/application-pws.yml: -------------------------------------------------------------------------------- 1 | cf: 2 | butlers: 3 | pws: cf-butler-clanging-beaver.apps.pivotal.io 4 | 5 | management: 6 | endpoints: 7 | web: 8 | exposure: 9 | include: info,health,metrics,scheduledtasks,loggers,prometheus 10 | -------------------------------------------------------------------------------- /samples/config-server.json: -------------------------------------------------------------------------------- 1 | { 2 | "git": { 3 | "uri": "https://github.com/cf-toolsuite/cf-hoover-config.git", 4 | "label": "main" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /scripts/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | export APP_NAME=cf-hoover 6 | 7 | cf push --no-start 8 | cf create-service p.config-server standard $APP_NAME-config -c config/config-server.json 9 | while [[ $(cf service $APP_NAME-config) != *"succeeded"* ]]; do 10 | echo "$APP_NAME-config is not ready yet..." 11 | sleep 5 12 | done 13 | cf bind-service $APP_NAME $APP_NAME-config 14 | cf start $APP_NAME -------------------------------------------------------------------------------- /scripts/deploy.with-registry.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | export APP_NAME=cf-hoover 6 | export REGISTRY_NAME=hooverRegistry 7 | 8 | cf push -f manifest.with-registry.yml --no-start "$1" 9 | cf create-service p.config-server standard $APP_NAME-config -c config/config-server.json 10 | cf create-service p.service-registry standard $REGISTRY_NAME 11 | for (( i = 0; i < 90; i++ )); do 12 | if [[ $(cf service $APP_NAME-config) != *"succeeded"* ]]; then 13 | echo "$APP_NAME-config is not ready yet..." 14 | sleep 10 15 | else 16 | break 17 | fi 18 | done 19 | cf bind-service $APP_NAME $APP_NAME-config 20 | for (( i = 0; i < 90; i++ )); do 21 | if [[ $(cf service $REGISTRY_NAME) != *"succeeded"* ]]; then 22 | echo "$REGISTRY_NAME is not ready yet..." 23 | sleep 10 24 | else 25 | break 26 | fi 27 | done 28 | cf bind-service $APP_NAME $REGISTRY_NAME 29 | cf start $APP_NAME -------------------------------------------------------------------------------- /scripts/destroy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -x 4 | 5 | export APP_NAME=cf-hoover 6 | 7 | cf app ${APP_NAME} --guid 8 | 9 | if [ $? -eq 0 ]; then 10 | cf stop $APP_NAME 11 | cf unbind-service $APP_NAME $APP_NAME-config 12 | cf delete-service $APP_NAME-config -f 13 | cf delete $APP_NAME -r -f 14 | else 15 | echo "$APP_NAME does not exist" 16 | fi 17 | -------------------------------------------------------------------------------- /scripts/destroy.with-registry.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -x 4 | 5 | export APP_NAME=cf-hoover 6 | export REGISTRY_NAME=hooverRegistry 7 | 8 | cf app ${APP_NAME} --guid 9 | 10 | if [ $? -eq 0 ]; then 11 | cf stop $APP_NAME 12 | cf unbind-service $APP_NAME $APP_NAME-config 13 | cf unbind-service $APP_NAME $REGISTRY_NAME 14 | cf delete-service $APP_NAME-config -f 15 | cf delete-service $REGISTRY_NAME -f 16 | cf delete $APP_NAME -r -f 17 | else 18 | echo "$APP_NAME does not exist" 19 | fi 20 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/AppInit.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.actuate.autoconfigure.security.reactive.EndpointRequest; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.boot.autoconfigure.security.reactive.PathRequest; 7 | import org.springframework.boot.context.properties.ConfigurationPropertiesScan; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.security.config.web.server.ServerHttpSecurity; 10 | import org.springframework.security.web.server.SecurityWebFilterChain; 11 | 12 | @SpringBootApplication 13 | @ConfigurationPropertiesScan 14 | public class AppInit { 15 | 16 | public static void main(String[] args) { 17 | SpringApplication.run(AppInit.class, args); 18 | } 19 | 20 | @Bean 21 | public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { 22 | return http 23 | .authorizeExchange() 24 | .matchers( 25 | PathRequest.toStaticResources().atCommonLocations(), 26 | EndpointRequest.toAnyEndpoint()) 27 | .permitAll() 28 | .pathMatchers("/accounting/**","/snapshot/**", "/space-users", "/users/**", "/collect") 29 | .permitAll() 30 | .and().build(); 31 | } 32 | } -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/client/DemographicsClient.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.client; 2 | 3 | import java.util.Map; 4 | import java.util.stream.Collectors; 5 | 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.stereotype.Service; 8 | import org.springframework.web.reactive.function.client.WebClient; 9 | import org.springframework.web.reactive.function.client.WebClientResponseException; 10 | 11 | import org.cftoolsuite.cfapp.config.HooverSettings; 12 | import org.cftoolsuite.cfapp.domain.Demographic; 13 | import org.cftoolsuite.cfapp.domain.Demographics; 14 | import lombok.extern.slf4j.Slf4j; 15 | import reactor.core.publisher.Flux; 16 | import reactor.core.publisher.Mono; 17 | 18 | @Slf4j 19 | @Service 20 | public class DemographicsClient { 21 | 22 | private final SnapshotClient snapshotClient; 23 | private final WebClient client; 24 | private final HooverSettings settings; 25 | 26 | @Autowired 27 | public DemographicsClient( 28 | SnapshotClient snapshotClient, 29 | WebClient client, 30 | HooverSettings settings) { 31 | this.snapshotClient = snapshotClient; 32 | this.client = client; 33 | this.settings = settings; 34 | } 35 | 36 | // We're going to get raw counts from each cf-butler instance registered w/ cf-hoover 37 | // User and service accounts represent a special case, we collect and merge sets before counting 38 | public Mono aggregateDemographics() { 39 | Mono serviceAccounts = snapshotClient.assembleServiceAccounts().flatMapMany(sa -> Flux.fromIterable(sa)).count(); 40 | Mono userAccounts = snapshotClient.assembleUserAccounts().flatMapMany(sa -> Flux.fromIterable(sa)).count(); 41 | Flux> butlers = Flux.fromIterable(settings.getButlers().entrySet()); 42 | return 43 | butlers 44 | .flatMap(b -> obtainDemographics(b.getKey(), b.getValue())) 45 | .collect(Collectors.toSet()) 46 | .map(dl -> Demographics 47 | .builder() 48 | .demographics(dl) 49 | .foundations(settings.getButlers().keySet().size())) 50 | .flatMap(b -> serviceAccounts.map(c -> b.serviceAccounts(c))) 51 | .flatMap(b -> userAccounts.map(c -> b.userAccounts(c).build())); 52 | } 53 | 54 | protected Mono obtainDemographics(String foundation, String baseUrl) { 55 | String uri = baseUrl + "/snapshot/demographics"; 56 | return client 57 | .get() 58 | .uri(uri) 59 | .retrieve() 60 | .bodyToMono(Demographic.class) 61 | .timeout(settings.getTimeout(), Mono.just(Demographic.builder().build())) 62 | .onErrorResume( 63 | WebClientResponseException.class, 64 | e -> { 65 | log.warn(String.format("Could not obtain Demographic from %s", uri), e); 66 | return Mono.just(Demographic.builder().build()); 67 | } 68 | ) 69 | .map(d -> Demographic 70 | .builder() 71 | .foundation(foundation) 72 | .organizations(d.getOrganizations()) 73 | .spaces(d.getSpaces()) 74 | .userAccounts(d.getUserAccounts()) 75 | .serviceAccounts(d.getServiceAccounts()) 76 | .build()); 77 | } 78 | 79 | } -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/client/OrganizationsClient.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.client; 2 | 3 | import java.util.List; 4 | import java.util.Map; 5 | 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.stereotype.Service; 8 | import org.springframework.web.reactive.function.client.WebClient; 9 | import org.springframework.web.reactive.function.client.WebClientResponseException; 10 | 11 | import org.cftoolsuite.cfapp.config.HooverSettings; 12 | import org.cftoolsuite.cfapp.domain.Organization; 13 | import lombok.extern.slf4j.Slf4j; 14 | import reactor.core.publisher.Flux; 15 | import reactor.core.publisher.Mono; 16 | 17 | @Slf4j 18 | @Service 19 | public class OrganizationsClient { 20 | 21 | private final WebClient client; 22 | private final HooverSettings settings; 23 | 24 | @Autowired 25 | public OrganizationsClient( 26 | WebClient client, 27 | HooverSettings settings) { 28 | this.client = client; 29 | this.settings = settings; 30 | } 31 | 32 | public Mono> assembleOrganizations() { 33 | Flux> butlers = Flux.fromIterable(settings.getButlers().entrySet()); 34 | Flux organizations = 35 | butlers.flatMap(b -> obtainOrganizations(b.getValue()) 36 | .flatMapMany(lo -> Flux.fromIterable(lo)) 37 | .map(o -> Organization.from(o).foundation(b.getKey()).build())); 38 | return organizations.collectList(); 39 | } 40 | 41 | protected Mono> obtainOrganizations(String baseUrl) { 42 | String uri = baseUrl + "/snapshot/organizations"; 43 | return client 44 | .get() 45 | .uri(uri) 46 | .retrieve() 47 | .toEntityList(Organization.class) 48 | .map(response -> response.getBody()) 49 | .timeout(settings.getTimeout(), Mono.empty()) 50 | .onErrorResume( 51 | WebClientResponseException.class, 52 | e -> { 53 | log.warn(String.format("Could not obtain organizations from %s", uri), e); 54 | return Mono.empty(); 55 | } 56 | ); 57 | } 58 | 59 | } -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/client/SnapshotClient.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.client; 2 | 3 | import java.util.List; 4 | import java.util.Map; 5 | import java.util.Set; 6 | import java.util.TreeSet; 7 | import java.util.stream.Collectors; 8 | 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.stereotype.Service; 11 | import org.springframework.web.reactive.function.client.WebClient; 12 | import org.springframework.web.reactive.function.client.WebClientResponseException; 13 | 14 | import org.cftoolsuite.cfapp.config.HooverSettings; 15 | import org.cftoolsuite.cfapp.domain.AppDetail; 16 | import org.cftoolsuite.cfapp.domain.AppRelationship; 17 | import org.cftoolsuite.cfapp.domain.ApplicationCounts; 18 | import org.cftoolsuite.cfapp.domain.ServiceInstanceCounts; 19 | import org.cftoolsuite.cfapp.domain.ServiceInstanceDetail; 20 | import org.cftoolsuite.cfapp.domain.SnapshotDetail; 21 | import org.cftoolsuite.cfapp.domain.SnapshotSummary; 22 | import org.cftoolsuite.cfapp.report.AppDetailCsvReport; 23 | import org.cftoolsuite.cfapp.report.ServiceInstanceDetailCsvReport; 24 | import org.cftoolsuite.cfapp.task.AppDetailRetrievedEvent; 25 | import org.cftoolsuite.cfapp.task.ServiceInstanceDetailRetrievedEvent; 26 | import lombok.extern.slf4j.Slf4j; 27 | import reactor.core.publisher.Flux; 28 | import reactor.core.publisher.Mono; 29 | 30 | @Slf4j 31 | @Service 32 | public class SnapshotClient { 33 | 34 | private final WebClient client; 35 | private final HooverSettings settings; 36 | 37 | @Autowired 38 | public SnapshotClient( 39 | WebClient client, 40 | HooverSettings settings) { 41 | this.client = client; 42 | this.settings = settings; 43 | } 44 | 45 | public Mono assembleCsvAIReport() { 46 | return assembleApplicationDetail() 47 | .map(r -> new AppDetailRetrievedEvent(this).detail(r)) 48 | .map(event -> new AppDetailCsvReport().generateDetail(event)); 49 | } 50 | 51 | public Mono assembleCsvSIReport() { 52 | return assembleServiceInstanceDetail() 53 | .map(r -> new ServiceInstanceDetailRetrievedEvent(this).detail(r)) 54 | .map(event -> new ServiceInstanceDetailCsvReport().generateDetail(event)); 55 | } 56 | 57 | public Mono> assembleApplicationDetail() { 58 | Flux> butlers = Flux.fromIterable(settings.getButlers().entrySet()); 59 | Flux detail = 60 | butlers.flatMap(b -> obtainSnapshotDetail(b.getValue()) 61 | .flatMapMany(sd -> Flux.fromIterable(sd.getApplications())) 62 | .map(ad -> AppDetail.from(ad).foundation(b.getKey()).build())); 63 | return detail.collectList(); 64 | } 65 | 66 | public Mono> assembleServiceInstanceDetail() { 67 | Flux> butlers = Flux.fromIterable(settings.getButlers().entrySet()); 68 | Flux detail = 69 | butlers.flatMap(b -> obtainSnapshotDetail(b.getValue()) 70 | .flatMapMany(sd -> Flux.fromIterable(sd.getServiceInstances())) 71 | .map(ad -> ServiceInstanceDetail.from(ad).foundation(b.getKey()).build())); 72 | return detail.collectList(); 73 | } 74 | 75 | public Mono> assembleApplicationRelationships() { 76 | Flux> butlers = Flux.fromIterable(settings.getButlers().entrySet()); 77 | Flux relations = 78 | butlers.flatMap(b -> obtainSnapshotDetail(b.getValue()) 79 | .flatMapMany(sd -> Flux.fromIterable(sd.getApplicationRelationships())) 80 | .map(ad -> AppRelationship.from(ad).foundation(b.getKey()).build())); 81 | return relations.collectList(); 82 | } 83 | 84 | protected Mono obtainSnapshotDetail(String baseUrl) { 85 | String uri = baseUrl + "/snapshot/detail"; 86 | return client 87 | .get() 88 | .uri(uri) 89 | .retrieve() 90 | .bodyToMono(SnapshotDetail.class) 91 | .timeout(settings.getTimeout(), Mono.just(SnapshotDetail.builder().build())) 92 | .onErrorResume( 93 | WebClientResponseException.class, 94 | e -> { 95 | log.warn(String.format("Could not obtain SnapshotDetail from %s", uri), e); 96 | return Mono.just(SnapshotDetail.builder().build()); 97 | } 98 | ); 99 | } 100 | 101 | protected Mono obtainSnapshotSummary(String baseUrl) { 102 | String uri = baseUrl + "/snapshot/summary"; 103 | return client 104 | .get() 105 | .uri(uri) 106 | .retrieve() 107 | .bodyToMono(SnapshotSummary.class) 108 | .timeout(settings.getTimeout(), Mono.just(SnapshotSummary.builder().build())) 109 | .onErrorResume( 110 | WebClientResponseException.class, 111 | e -> { 112 | log.warn(String.format("Could not obtain SnapshotSummary from %s", uri), e); 113 | return Mono.just(SnapshotSummary.builder().build()); 114 | } 115 | ); 116 | } 117 | 118 | public Mono assembleSnapshotDetail() { 119 | return assembleApplicationDetail() 120 | .map(ad -> SnapshotDetail.builder().applications(ad)) 121 | .flatMap(b -> assembleServiceInstanceDetail() 122 | .map(sid -> b.serviceInstances(sid))) 123 | .flatMap(b -> assembleApplicationRelationships() 124 | .map(ar -> b.applicationRelationships(ar))) 125 | .flatMap(b -> assembleUserAccounts() 126 | .map(ua -> b.userAccounts(ua))) 127 | .flatMap(b -> assembleServiceAccounts() 128 | .map(sa -> b.serviceAccounts(sa).build())); 129 | } 130 | 131 | public Mono assembleSnapshotSummary() { 132 | return assembleApplicationCounts() 133 | .map(ac -> SnapshotSummary.builder().applicationCounts(ac)) 134 | .flatMap(b -> assembleServiceInstanceCounts().map(sic -> b.serviceInstanceCounts(sic).build())); 135 | } 136 | 137 | public Mono> assembleUserAccounts() { 138 | Flux> butlers = Flux.fromIterable(settings.getButlers().entrySet()); 139 | Flux accounts = 140 | butlers.flatMap(b -> obtainSnapshotDetail(b.getValue()) 141 | .flatMapMany(sd -> Flux.fromIterable(sd.getUserAccounts()))); 142 | return accounts.collect(Collectors.toCollection(TreeSet::new)); 143 | } 144 | 145 | public Mono> assembleServiceAccounts() { 146 | Flux> butlers = Flux.fromIterable(settings.getButlers().entrySet()); 147 | Flux accounts = 148 | butlers.flatMap(b -> obtainSnapshotDetail(b.getValue()) 149 | .flatMapMany(sd -> Flux.fromIterable(sd.getServiceAccounts()))); 150 | return accounts.collect(Collectors.toCollection(TreeSet::new)); 151 | } 152 | 153 | protected Mono assembleApplicationCounts() { 154 | Flux> butlers = Flux.fromIterable(settings.getButlers().entrySet()); 155 | return butlers 156 | .flatMap(b -> obtainSnapshotSummary(b.getValue())) 157 | .map(sd -> sd.getApplicationCounts()) 158 | .collectList() 159 | .map(acl -> ApplicationCounts.aggregate(acl)); 160 | } 161 | 162 | protected Mono assembleServiceInstanceCounts() { 163 | Flux> butlers = Flux.fromIterable(settings.getButlers().entrySet()); 164 | return butlers 165 | .flatMap(b -> obtainSnapshotSummary(b.getValue())) 166 | .map(sd -> sd.getServiceInstanceCounts()) 167 | .collectList() 168 | .map(acl -> ServiceInstanceCounts.aggregate(acl)); 169 | } 170 | } -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/client/SpaceUsersClient.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.client; 2 | 3 | import java.util.Map; 4 | import java.util.Set; 5 | import java.util.stream.Collectors; 6 | 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.stereotype.Service; 9 | import org.springframework.web.reactive.function.client.WebClient; 10 | import org.springframework.web.reactive.function.client.WebClientResponseException; 11 | 12 | import org.cftoolsuite.cfapp.config.HooverSettings; 13 | import org.cftoolsuite.cfapp.domain.SpaceUsers; 14 | import lombok.extern.slf4j.Slf4j; 15 | import reactor.core.publisher.Flux; 16 | import reactor.core.publisher.Mono; 17 | 18 | @Slf4j 19 | @Service 20 | public class SpaceUsersClient { 21 | 22 | private final WebClient client; 23 | private final HooverSettings settings; 24 | private final SnapshotClient snapshotClient; 25 | 26 | @Autowired 27 | public SpaceUsersClient( 28 | WebClient client, 29 | HooverSettings settings, 30 | SnapshotClient snapshotClient) { 31 | this.client = client; 32 | this.settings = settings; 33 | this.snapshotClient = snapshotClient; 34 | } 35 | 36 | public Flux findAll() { 37 | Flux> butlers = Flux.fromIterable(settings.getButlers().entrySet()); 38 | return 39 | butlers.flatMap(b -> obtainSpaceUsers(b.getValue()) 40 | .map(su -> SpaceUsers.from(su).foundation(b.getKey()).build())); 41 | } 42 | 43 | protected Flux obtainSpaceUsers(String baseUrl) { 44 | String uri = baseUrl + "/snapshot/spaces/users"; 45 | return client 46 | .get() 47 | .uri(uri) 48 | .retrieve() 49 | .bodyToFlux(SpaceUsers.class) 50 | .timeout(settings.getTimeout(), Flux.just(SpaceUsers.builder().build())) 51 | .onErrorResume( 52 | WebClientResponseException.class, 53 | e -> { 54 | log.warn(String.format("Could not obtain SpaceUsers from %s", uri), e); 55 | return Flux.just(SpaceUsers.builder().build()); 56 | } 57 | ); 58 | } 59 | 60 | public Mono> obtainAccountNames() { 61 | return snapshotClient.assembleSnapshotDetail() 62 | .flatMapMany( 63 | sd -> 64 | Flux.concat( 65 | Flux.fromIterable(sd.getServiceAccounts()), 66 | Flux.fromIterable(sd.getUserAccounts()) 67 | ) 68 | ) 69 | .collect(Collectors.toSet()); 70 | } 71 | 72 | public Mono totalAccounts() { 73 | return obtainAccountNames() 74 | .flatMapMany(s -> Flux.fromIterable(s)) 75 | .count(); 76 | } 77 | 78 | } -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/client/SpacesClient.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.client; 2 | 3 | import java.util.List; 4 | import java.util.Map; 5 | 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.stereotype.Service; 8 | import org.springframework.web.reactive.function.client.WebClient; 9 | import org.springframework.web.reactive.function.client.WebClientResponseException; 10 | 11 | import org.cftoolsuite.cfapp.config.HooverSettings; 12 | import org.cftoolsuite.cfapp.domain.Space; 13 | import lombok.extern.slf4j.Slf4j; 14 | import reactor.core.publisher.Flux; 15 | import reactor.core.publisher.Mono; 16 | 17 | @Slf4j 18 | @Service 19 | public class SpacesClient { 20 | 21 | private final WebClient client; 22 | private final HooverSettings settings; 23 | 24 | @Autowired 25 | public SpacesClient( 26 | WebClient client, 27 | HooverSettings settings) { 28 | this.client = client; 29 | this.settings = settings; 30 | } 31 | 32 | public Mono> assembleSpaces() { 33 | Flux> butlers = Flux.fromIterable(settings.getButlers().entrySet()); 34 | Flux spaces = 35 | butlers.flatMap(b -> obtainSpaces(b.getValue()) 36 | .flatMapMany(ls -> Flux.fromIterable(ls)) 37 | .map(s -> Space.from(s).foundation(b.getKey()).build())); 38 | return spaces.collectList(); 39 | } 40 | 41 | protected Mono> obtainSpaces(String baseUrl) { 42 | String uri = baseUrl + "/snapshot/spaces"; 43 | return client 44 | .get() 45 | .uri(uri) 46 | .retrieve() 47 | .toEntityList(Space.class) 48 | .map(response -> response.getBody()) 49 | .timeout(settings.getTimeout(), Mono.empty()) 50 | .onErrorResume( 51 | WebClientResponseException.class, 52 | e -> { 53 | log.warn(String.format("Could not obtain spaces from %s", uri), e); 54 | return Mono.empty(); 55 | } 56 | ); 57 | } 58 | 59 | } -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/client/SpringApplicationClient.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.client; 2 | 3 | import java.util.Collections; 4 | import java.util.List; 5 | import java.util.Map; 6 | import java.util.stream.Collectors; 7 | 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.core.ParameterizedTypeReference; 10 | import org.springframework.stereotype.Service; 11 | import org.springframework.web.reactive.function.client.WebClient; 12 | import org.springframework.web.reactive.function.client.WebClientResponseException; 13 | 14 | import org.cftoolsuite.cfapp.config.HooverSettings; 15 | import org.cftoolsuite.cfapp.domain.JavaAppDetail; 16 | import lombok.extern.slf4j.Slf4j; 17 | import reactor.core.publisher.Flux; 18 | import reactor.core.publisher.Mono; 19 | 20 | @Slf4j 21 | @Service 22 | public class SpringApplicationClient { 23 | 24 | private final WebClient client; 25 | private final HooverSettings settings; 26 | 27 | @Autowired 28 | public SpringApplicationClient( 29 | WebClient client, 30 | HooverSettings settings) { 31 | this.client = client; 32 | this.settings = settings; 33 | } 34 | 35 | 36 | public Mono> assembleSpringApplicationDetail() { 37 | Flux> butlers = Flux.fromIterable(settings.getButlers().entrySet()); 38 | return 39 | butlers 40 | .flatMap(b -> obtainSpringApplicationDetail(b.getValue()) 41 | .map(ad -> JavaAppDetail.from(ad).foundation(b.getKey()).build())) 42 | .collectList(); 43 | } 44 | 45 | protected Flux obtainSpringApplicationDetail(String baseUrl) { 46 | String uri = baseUrl + "/snapshot/detail/ai/spring"; 47 | return client 48 | .get() 49 | .uri(uri) 50 | .retrieve() 51 | .bodyToFlux(JavaAppDetail.class) 52 | .timeout(settings.getTimeout(), Mono.just(JavaAppDetail.builder().build())) 53 | .onErrorResume( 54 | WebClientResponseException.class, 55 | e -> { 56 | log.warn(String.format("Could not obtain Spring application details from %s", uri), e); 57 | return Mono.just(JavaAppDetail.builder().build()); 58 | } 59 | ); 60 | } 61 | 62 | public Mono> calculateSpringApplicationDependencyFrequency() { 63 | Flux> butlers = Flux.fromIterable(settings.getButlers().entrySet()); 64 | return 65 | butlers 66 | .flatMap(b -> obtainSpringApplicationDependencyFrequency(b.getValue())) 67 | .flatMapIterable(Map::entrySet) 68 | .collect(Collectors.toMap( 69 | Map.Entry::getKey, 70 | Map.Entry::getValue, 71 | Integer::sum 72 | )); 73 | } 74 | 75 | protected Mono> obtainSpringApplicationDependencyFrequency(String baseUrl) { 76 | String uri = baseUrl + "/snapshot/summary/ai/spring"; 77 | return client 78 | .get() 79 | .uri(uri) 80 | .retrieve() 81 | .bodyToMono(new ParameterizedTypeReference>() {}) 82 | .timeout(settings.getTimeout(), Mono.just(Collections.emptyMap())) 83 | .onErrorResume( 84 | WebClientResponseException.class, 85 | e -> { 86 | log.warn(String.format("Could not obtain Spring dependency frequency from %s", uri), e); 87 | return Mono.just(Collections.emptyMap()); 88 | } 89 | ); 90 | } 91 | 92 | } -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/client/TimeKeeperClient.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.client; 2 | 3 | import java.time.LocalDateTime; 4 | import java.time.format.DateTimeFormatter; 5 | import java.util.Map; 6 | import java.util.Set; 7 | 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.stereotype.Service; 10 | import org.springframework.web.reactive.function.client.WebClient; 11 | import org.springframework.web.reactive.function.client.WebClientResponseException; 12 | 13 | import org.cftoolsuite.cfapp.config.HooverSettings; 14 | import org.cftoolsuite.cfapp.domain.TimeKeeper; 15 | import org.cftoolsuite.cfapp.domain.TimeKeepers; 16 | import lombok.extern.slf4j.Slf4j; 17 | import reactor.core.publisher.Flux; 18 | import reactor.core.publisher.Mono; 19 | 20 | @Slf4j 21 | @Service 22 | public class TimeKeeperClient { 23 | 24 | private final WebClient client; 25 | private final HooverSettings settings; 26 | 27 | @Autowired 28 | public TimeKeeperClient( 29 | WebClient client, 30 | HooverSettings settings) { 31 | this.client = client; 32 | this.settings = settings; 33 | } 34 | 35 | public Mono> assembleDateTimeCollection() { 36 | Flux> butlers = Flux.fromIterable(settings.getButlers().entrySet()); 37 | Flux result = 38 | butlers.flatMap(b -> obtainDateTimeCollected(b.getValue()) 39 | .map(dtc -> TimeKeeper.builder().foundation(b.getKey()).collectionDateTime(dtc).build())); 40 | return result.collectList().map(l -> Set.copyOf(l)); 41 | } 42 | 43 | protected Mono obtainDateTimeCollected(String baseUrl) { 44 | String uri = baseUrl + "/collect"; 45 | return client 46 | .get() 47 | .uri(uri) 48 | .retrieve() 49 | .bodyToMono(String.class) 50 | .map(s -> LocalDateTime.parse(s, DateTimeFormatter.ISO_LOCAL_DATE_TIME)) 51 | .timeout(settings.getTimeout(), Mono.empty()) 52 | .onErrorResume( 53 | WebClientResponseException.class, 54 | e -> { 55 | log.warn(String.format("Could not obtain X-DateTime-Collected from %s", uri), e); 56 | return Mono.empty(); 57 | } 58 | ); 59 | } 60 | 61 | public Mono assembleTimeKeepers() { 62 | return assembleDateTimeCollection() 63 | .map(tk -> TimeKeepers.builder().timeKeepers(tk).build()); 64 | } 65 | 66 | } -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/client/UsageClient.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.client; 2 | 3 | import java.util.Map; 4 | 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.stereotype.Service; 7 | import org.springframework.web.reactive.function.client.WebClient; 8 | import org.springframework.web.reactive.function.client.WebClientResponseException; 9 | 10 | import org.cftoolsuite.cfapp.config.HooverSettings; 11 | import org.cftoolsuite.cfapp.domain.accounting.application.AppUsageReport; 12 | import org.cftoolsuite.cfapp.domain.accounting.service.ServiceUsageReport; 13 | import org.cftoolsuite.cfapp.domain.accounting.task.TaskUsageReport; 14 | import lombok.extern.slf4j.Slf4j; 15 | import reactor.core.publisher.Flux; 16 | import reactor.core.publisher.Mono; 17 | 18 | // @see https://docs.pivotal.io/pivotalcf/2-4/opsguide/accounting-report.html 19 | 20 | @Slf4j 21 | @Service 22 | public class UsageClient { 23 | 24 | private final WebClient client; 25 | private final HooverSettings settings; 26 | 27 | @Autowired 28 | public UsageClient( 29 | WebClient client, 30 | HooverSettings settings) { 31 | this.client = client; 32 | this.settings = settings; 33 | } 34 | 35 | protected Mono getTaskReport(String butlerRoute) { 36 | String uri = butlerRoute + "/accounting/tasks"; 37 | return client 38 | .get() 39 | .uri(uri) 40 | .retrieve() 41 | .bodyToMono(TaskUsageReport.class) 42 | .timeout(settings.getTimeout(), Mono.just(TaskUsageReport.builder().build())) 43 | .onErrorResume( 44 | WebClientResponseException.class, 45 | e -> { 46 | log.warn(String.format("Could not obtain TaskUsageReport from %s", uri), e); 47 | return Mono.just(TaskUsageReport.builder().build()); 48 | } 49 | ); 50 | } 51 | 52 | protected Mono getApplicationReport(String butlerRoute) { 53 | String uri = butlerRoute + "/accounting/applications"; 54 | return client 55 | .get() 56 | .uri(uri) 57 | .retrieve() 58 | .bodyToMono(AppUsageReport.class) 59 | .timeout(settings.getTimeout(), Mono.just(AppUsageReport.builder().build())) 60 | .onErrorResume( 61 | WebClientResponseException.class, 62 | e -> { 63 | log.warn(String.format("Could not obtain AppUsageReport from %s", uri), e); 64 | return Mono.just(AppUsageReport.builder().build()); 65 | } 66 | ); 67 | } 68 | 69 | protected Mono getServiceReport(String butlerRoute) { 70 | String uri = butlerRoute + "/accounting/services"; 71 | return client 72 | .get() 73 | .uri(uri) 74 | .retrieve() 75 | .bodyToMono(ServiceUsageReport.class) 76 | .timeout(settings.getTimeout(), Mono.just(ServiceUsageReport.builder().build())) 77 | .onErrorResume( 78 | WebClientResponseException.class, 79 | e -> { 80 | log.warn(String.format("Could not obtain ServiceUsageReport from %s", uri), e); 81 | return Mono.just(ServiceUsageReport.builder().build()); 82 | } 83 | ); 84 | } 85 | 86 | public Mono getTaskReport() { 87 | Flux> butlers = Flux.fromIterable(settings.getButlers().entrySet()); 88 | return butlers.flatMap(b -> getTaskReport(b.getValue())) 89 | .collectList() 90 | .map(l -> TaskUsageReport.aggregate(l)); 91 | } 92 | 93 | public Mono getApplicationReport() { 94 | Flux> butlers = Flux.fromIterable(settings.getButlers().entrySet()); 95 | return butlers.flatMap(b -> getApplicationReport(b.getValue())) 96 | .collectList() 97 | .map(l -> AppUsageReport.aggregate(l)); 98 | } 99 | 100 | public Mono getServiceReport() { 101 | Flux> butlers = Flux.fromIterable(settings.getButlers().entrySet()); 102 | return butlers.flatMap(b -> getServiceReport(b.getValue())) 103 | .collectList() 104 | .map(l -> ServiceUsageReport.aggregate(l)); 105 | } 106 | } -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/config/HooverSettings.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.config; 2 | 3 | import java.time.Duration; 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | 7 | import org.springframework.boot.context.properties.ConfigurationProperties; 8 | import org.springframework.cloud.context.config.annotation.RefreshScope; 9 | 10 | import lombok.Data; 11 | 12 | @Data 13 | @RefreshScope 14 | @ConfigurationProperties(prefix = "cf") 15 | public class HooverSettings { 16 | 17 | private Map butlers = new HashMap<>(); 18 | private boolean sslValidationSkipped; 19 | private Duration timeout = Duration.ofMinutes(2); 20 | 21 | public void setButlers(Map butlers) { 22 | butlers.replaceAll((k, v) -> butlerURL(v)); 23 | this.butlers = butlers; 24 | } 25 | 26 | private String butlerURL(String url) { 27 | if (!url.startsWith("http")) { 28 | url = "https://" + url; 29 | } 30 | return url; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/config/WebClientConfig.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.config; 2 | 3 | import javax.net.ssl.SSLException; 4 | 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 7 | import org.springframework.cloud.context.config.annotation.RefreshScope; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | import org.springframework.http.client.reactive.ReactorClientHttpConnector; 11 | import org.springframework.web.reactive.function.client.ExchangeStrategies; 12 | import org.springframework.web.reactive.function.client.WebClient; 13 | 14 | import io.netty.handler.ssl.SslContext; 15 | import io.netty.handler.ssl.SslContextBuilder; 16 | import io.netty.handler.ssl.util.InsecureTrustManagerFactory; 17 | import reactor.netty.http.client.HttpClient; 18 | 19 | @RefreshScope 20 | @Configuration 21 | public class WebClientConfig { 22 | 23 | // @see https://stackoverflow.com/questions/45418523/spring-5-webclient-using-ssl/53147631#53147631 24 | 25 | @Bean 26 | @ConditionalOnProperty(name = "cf.sslValidationSkipped", havingValue="true") 27 | public WebClient insecureWebClient( 28 | WebClient.Builder builder, 29 | @Value("${spring.codec.max-in-memory-size}") Integer maxInMemorySize) throws SSLException { 30 | SslContext context = 31 | SslContextBuilder 32 | .forClient() 33 | .trustManager(InsecureTrustManagerFactory.INSTANCE) 34 | .build(); 35 | HttpClient httpClient = 36 | HttpClient 37 | .create() 38 | .secure(t -> t.sslContext(context)); 39 | return 40 | builder 41 | .exchangeStrategies( 42 | ExchangeStrategies 43 | .builder() 44 | .codecs( 45 | configurer -> 46 | configurer 47 | .defaultCodecs() 48 | .maxInMemorySize(maxInMemorySize) 49 | ) 50 | .build() 51 | ) 52 | .clientConnector(new ReactorClientHttpConnector(httpClient)) 53 | .build(); 54 | } 55 | 56 | @Bean 57 | @ConditionalOnProperty(name = "cf.sslValidationSkipped", havingValue="false", matchIfMissing=true) 58 | public WebClient secureWebClient( 59 | WebClient.Builder builder, 60 | @Value("${spring.codec.max-in-memory-size}") Integer maxInMemorySize) { 61 | return 62 | builder 63 | .exchangeStrategies( 64 | ExchangeStrategies 65 | .builder() 66 | .codecs( 67 | configurer -> 68 | configurer 69 | .defaultCodecs() 70 | .maxInMemorySize(maxInMemorySize) 71 | ) 72 | .build() 73 | ) 74 | .build(); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/controller/DemographicsController.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.controller; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.http.ResponseEntity; 5 | import org.springframework.web.bind.annotation.GetMapping; 6 | import org.springframework.web.bind.annotation.RestController; 7 | 8 | import org.cftoolsuite.cfapp.domain.Demographics; 9 | import org.cftoolsuite.cfapp.client.DemographicsClient; 10 | import reactor.core.publisher.Mono; 11 | 12 | @RestController 13 | public class DemographicsController { 14 | 15 | private final DemographicsClient client; 16 | 17 | @Autowired 18 | public DemographicsController(DemographicsClient client) { 19 | this.client = client; 20 | } 21 | 22 | @GetMapping("/snapshot/demographics") 23 | public Mono> aggregateDemographics() { 24 | return client.aggregateDemographics() 25 | .map(d -> ResponseEntity.ok(d)) 26 | .defaultIfEmpty(ResponseEntity.notFound().build()); 27 | } 28 | } -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/controller/OrganizationsController.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.controller; 2 | 3 | import java.util.List; 4 | 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | import org.springframework.web.bind.annotation.RestController; 9 | 10 | import org.cftoolsuite.cfapp.client.OrganizationsClient; 11 | import org.cftoolsuite.cfapp.domain.Organization; 12 | import reactor.core.publisher.Mono; 13 | 14 | @RestController 15 | public class OrganizationsController { 16 | 17 | private final OrganizationsClient client; 18 | 19 | @Autowired 20 | public OrganizationsController(OrganizationsClient client) { 21 | this.client = client; 22 | } 23 | 24 | @GetMapping("/snapshot/organizations") 25 | public Mono>> assembleOrganizations() { 26 | return client.assembleOrganizations() 27 | .map(d -> ResponseEntity.ok(d)) 28 | .defaultIfEmpty(ResponseEntity.notFound().build()); 29 | } 30 | } -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/controller/SnapshotController.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.controller; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.http.HttpStatus; 5 | import org.springframework.http.MediaType; 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | import org.springframework.web.bind.annotation.RestController; 9 | 10 | import org.cftoolsuite.cfapp.domain.SnapshotDetail; 11 | import org.cftoolsuite.cfapp.domain.SnapshotSummary; 12 | import org.cftoolsuite.cfapp.client.SnapshotClient; 13 | import reactor.core.publisher.Mono; 14 | 15 | @RestController 16 | public class SnapshotController { 17 | 18 | private final SnapshotClient client; 19 | 20 | @Autowired 21 | public SnapshotController( 22 | SnapshotClient client) { 23 | this.client = client; 24 | } 25 | 26 | @GetMapping("/snapshot/detail") 27 | public Mono> getDetail() { 28 | return client 29 | .assembleSnapshotDetail() 30 | .map(detail -> ResponseEntity.ok(detail)) 31 | .defaultIfEmpty(new ResponseEntity<>(HttpStatus.NOT_FOUND)); 32 | } 33 | 34 | @GetMapping("/snapshot/summary") 35 | public Mono> getSummary() { 36 | return client 37 | .assembleSnapshotSummary() 38 | .map(summary -> ResponseEntity.ok(summary)) 39 | .defaultIfEmpty(new ResponseEntity<>(HttpStatus.NOT_FOUND)); 40 | } 41 | 42 | @GetMapping(value = { "/snapshot/detail/si" }, produces = MediaType.TEXT_PLAIN_VALUE ) 43 | public Mono> getServiceInstanceCsvReport() { 44 | return client 45 | .assembleCsvSIReport() 46 | .map(r -> ResponseEntity.ok(r)); 47 | } 48 | 49 | @GetMapping(value = { "/snapshot/detail/ai" }, produces = MediaType.TEXT_PLAIN_VALUE ) 50 | public Mono> getApplicationInstanceCsvReport() { 51 | return client 52 | .assembleCsvAIReport() 53 | .map(r -> ResponseEntity.ok(r)); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/controller/SpaceUsersController.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.controller; 2 | 3 | import java.util.List; 4 | import java.util.Set; 5 | 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.http.HttpStatus; 8 | import org.springframework.http.ResponseEntity; 9 | import org.springframework.web.bind.annotation.GetMapping; 10 | import org.springframework.web.bind.annotation.PathVariable; 11 | import org.springframework.web.bind.annotation.RestController; 12 | 13 | import org.cftoolsuite.cfapp.domain.SpaceUsers; 14 | import org.cftoolsuite.cfapp.client.SpaceUsersClient; 15 | import reactor.core.publisher.Flux; 16 | import reactor.core.publisher.Mono; 17 | 18 | @RestController 19 | public class SpaceUsersController { 20 | 21 | private final SpaceUsersClient client; 22 | 23 | @Autowired 24 | public SpaceUsersController( 25 | SpaceUsersClient client) { 26 | this.client = client; 27 | } 28 | 29 | @GetMapping("/snapshot/spaces/users") 30 | public Mono>> getAllSpaceUsers() { 31 | return client 32 | .findAll() 33 | .collectList() 34 | .map(users -> ResponseEntity.ok(users)) 35 | .defaultIfEmpty(new ResponseEntity<>(HttpStatus.NOT_FOUND)); 36 | } 37 | 38 | @GetMapping("/snapshot/{foundation}/{organization}/{space}/users") 39 | public Mono> getUsersInOrganizationAndSpace( 40 | @PathVariable("foundation") String foundation, 41 | @PathVariable("organization") String organization, 42 | @PathVariable("space") String space) { 43 | return getAllSpaceUsers() 44 | .flatMapMany(r -> Flux.fromIterable(r.getBody())) 45 | .filter(su -> 46 | su.getFoundation().equals(foundation) && 47 | su.getOrganization().equals(organization) && 48 | su.getSpace().equals(space)) 49 | .singleOrEmpty() 50 | .map(users -> ResponseEntity.ok(users)) 51 | .defaultIfEmpty(ResponseEntity.notFound().build()); 52 | } 53 | 54 | @GetMapping("/snapshot/users/count") 55 | public Mono> totalAccounts() { 56 | return client 57 | .totalAccounts() 58 | .map(t -> ResponseEntity.ok(t)) 59 | .defaultIfEmpty(ResponseEntity.notFound().build()); 60 | 61 | } 62 | 63 | @GetMapping("/snapshot/users") 64 | public Mono>> getAllAccountNames() { 65 | return client 66 | .obtainAccountNames() 67 | .map(r -> ResponseEntity.ok(r)) 68 | .defaultIfEmpty(ResponseEntity.notFound().build()); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/controller/SpacesController.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.controller; 2 | 3 | import java.util.List; 4 | 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | import org.springframework.web.bind.annotation.RestController; 9 | 10 | import org.cftoolsuite.cfapp.client.SpacesClient; 11 | import org.cftoolsuite.cfapp.domain.Space; 12 | import reactor.core.publisher.Mono; 13 | 14 | @RestController 15 | public class SpacesController { 16 | 17 | private final SpacesClient client; 18 | 19 | @Autowired 20 | public SpacesController(SpacesClient client) { 21 | this.client = client; 22 | } 23 | 24 | @GetMapping("/snapshot/spaces") 25 | public Mono>> assembleSpaces() { 26 | return client.assembleSpaces() 27 | .map(d -> ResponseEntity.ok(d)) 28 | .defaultIfEmpty(ResponseEntity.notFound().build()); 29 | } 30 | } -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/controller/SpringApplicationController.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.controller; 2 | 3 | import java.util.List; 4 | import java.util.Map; 5 | 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.http.ResponseEntity; 8 | import org.springframework.web.bind.annotation.GetMapping; 9 | import org.springframework.web.bind.annotation.RestController; 10 | 11 | import org.cftoolsuite.cfapp.client.SpringApplicationClient; 12 | import org.cftoolsuite.cfapp.domain.JavaAppDetail; 13 | import reactor.core.publisher.Mono; 14 | 15 | @RestController 16 | public class SpringApplicationController { 17 | 18 | private final SpringApplicationClient client; 19 | 20 | @Autowired 21 | public SpringApplicationController(SpringApplicationClient client) { 22 | this.client = client; 23 | } 24 | 25 | @GetMapping("/snapshot/detail/ai/spring") 26 | public Mono>> assembleSpringApplicationDetail() { 27 | return client.assembleSpringApplicationDetail() 28 | .map(d -> ResponseEntity.ok(d)) 29 | .defaultIfEmpty(ResponseEntity.notFound().build()); 30 | } 31 | 32 | @GetMapping("/snapshot/summary/ai/spring") 33 | public Mono>> calculateSpringApplicationDependencyFrequency() { 34 | return client.calculateSpringApplicationDependencyFrequency() 35 | .map(d -> ResponseEntity.ok(d)) 36 | .defaultIfEmpty(ResponseEntity.notFound().build()); 37 | } 38 | } -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/controller/TimeKeeperController.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.controller; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.http.HttpStatus; 5 | import org.springframework.http.ResponseEntity; 6 | import org.springframework.web.bind.annotation.GetMapping; 7 | import org.springframework.web.bind.annotation.RestController; 8 | 9 | import org.cftoolsuite.cfapp.client.TimeKeeperClient; 10 | import org.cftoolsuite.cfapp.domain.TimeKeepers; 11 | import reactor.core.publisher.Mono; 12 | 13 | @RestController 14 | public class TimeKeeperController { 15 | 16 | private final TimeKeeperClient client; 17 | 18 | @Autowired 19 | public TimeKeeperController( 20 | TimeKeeperClient client) { 21 | this.client = client; 22 | } 23 | 24 | @GetMapping("/collect") 25 | public Mono> getTimeKeepers() { 26 | return client 27 | .assembleTimeKeepers() 28 | .map(tk -> ResponseEntity.ok(tk)) 29 | .defaultIfEmpty(new ResponseEntity<>(HttpStatus.NOT_FOUND)); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/controller/UsageController.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.controller; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.http.ResponseEntity; 5 | import org.springframework.web.bind.annotation.GetMapping; 6 | import org.springframework.web.bind.annotation.RestController; 7 | 8 | import org.cftoolsuite.cfapp.domain.accounting.application.AppUsageReport; 9 | import org.cftoolsuite.cfapp.domain.accounting.service.ServiceUsageReport; 10 | import org.cftoolsuite.cfapp.domain.accounting.task.TaskUsageReport; 11 | import org.cftoolsuite.cfapp.client.UsageClient; 12 | import reactor.core.publisher.Mono; 13 | 14 | @RestController 15 | public class UsageController { 16 | 17 | private final UsageClient client; 18 | 19 | @Autowired 20 | public UsageController(UsageClient client) { 21 | this.client = client; 22 | } 23 | 24 | @GetMapping(value = "/accounting/tasks") 25 | public Mono> getTaskReport() { 26 | return client 27 | .getTaskReport() 28 | .map(r -> ResponseEntity.ok(r)) 29 | .defaultIfEmpty(ResponseEntity.notFound().build()); 30 | } 31 | 32 | @GetMapping(value = "/accounting/applications") 33 | public Mono> getApplicationReport() { 34 | return client 35 | .getApplicationReport() 36 | .map(r -> ResponseEntity.ok(r)) 37 | .defaultIfEmpty(ResponseEntity.notFound().build()); 38 | } 39 | 40 | @GetMapping(value = "/accounting/services") 41 | public Mono> getServiceReport() { 42 | return client 43 | .getServiceReport() 44 | .map(r -> ResponseEntity.ok(r)) 45 | .defaultIfEmpty(ResponseEntity.notFound().build()); 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/AppDetail.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain; 2 | 3 | import java.time.LocalDateTime; 4 | import java.util.Collections; 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | import org.apache.commons.lang3.StringUtils; 9 | 10 | import lombok.AccessLevel; 11 | import lombok.AllArgsConstructor; 12 | import lombok.Builder; 13 | import lombok.Builder.Default; 14 | import lombok.EqualsAndHashCode; 15 | import lombok.Getter; 16 | import lombok.NoArgsConstructor; 17 | import lombok.ToString; 18 | 19 | @Builder 20 | @AllArgsConstructor(access=AccessLevel.PACKAGE) 21 | @NoArgsConstructor(access=AccessLevel.PACKAGE) 22 | @Getter 23 | @EqualsAndHashCode 24 | @ToString 25 | public class AppDetail { 26 | 27 | private String foundation; 28 | private String organization; 29 | private String space; 30 | private String appId; 31 | private String appName; 32 | private String buildpack; 33 | private String buildpackVersion; 34 | private String image; 35 | private String stack; 36 | @Default 37 | private Integer runningInstances = 0; 38 | @Default 39 | private Integer totalInstances = 0; 40 | @Default 41 | private Long memoryUsed = 0L; 42 | @Default 43 | private Long memoryQuota = 0L; 44 | @Default 45 | private Long diskUsed = 0L; 46 | @Default 47 | private Long diskQuota = 0L; 48 | @Default 49 | private List urls = new ArrayList<>(); 50 | private LocalDateTime lastPushed; 51 | private String lastEvent; 52 | private String lastEventActor; 53 | private LocalDateTime lastEventTime; 54 | private String buildpackReleaseType; 55 | private LocalDateTime buildpackReleaseDate; 56 | private String buildpackLatestVersion; 57 | private String buildpackLatestUrl; 58 | private String requestedState; 59 | 60 | public String toCsv() { 61 | return String.join(",", wrap(getFoundation()), wrap(getOrganization()), wrap(getSpace()), wrap(getAppId()), wrap(getAppName()), 62 | wrap(getBuildpack()), wrap(getBuildpackVersion()), wrap(getImage()), wrap(getStack()), wrap(String.valueOf(getRunningInstances())), 63 | wrap(String.valueOf(getTotalInstances())), wrap(Double.toString(toGigabytes(getMemoryUsed()))), wrap(Double.toString(toGigabytes(getMemoryQuota()))), 64 | wrap(Double.toString(toGigabytes(getDiskUsed()))), wrap(Double.toString(toGigabytes(getDiskQuota()))), 65 | (wrap(String.join(",", getUrls() != null ? getUrls(): Collections.emptyList()))), 66 | wrap(getLastPushed() != null ? getLastPushed().toString() : ""), wrap(getLastEvent()), 67 | wrap(getLastEventActor()), wrap(getLastEventTime() != null ? getLastEventTime().toString() : ""), 68 | wrap(getRequestedState()), 69 | wrap(getBuildpackReleaseType()), 70 | wrap(getBuildpackReleaseDate() != null ? getBuildpackReleaseDate().toString() : ""), 71 | wrap(getBuildpackLatestVersion()), 72 | wrap(getBuildpackLatestUrl())); 73 | } 74 | 75 | private static String wrap(String value) { 76 | return value != null ? StringUtils.wrap(value, '"') : StringUtils.wrap("", '"'); 77 | } 78 | 79 | private Double toGigabytes(Long input) { 80 | return Double.valueOf(input / Math.pow(1024, 3)); 81 | } 82 | 83 | public static String headers() { 84 | return String.join(",", "foundation", "organization", "space", "application id", "application name", "buildpack", "buildpack version", "image", 85 | "stack", "running instances", "total instances", "memory used (in gb)", "memory quota (in gb)", "disk used (in gb)", "disk quota (in gb)", "urls", "last pushed", "last event", 86 | "last event actor", "last event time", "requested state", 87 | "latest buildpack release type", "latest buildpack release date", "latest buildpack version", "latest buildpack Url" ); 88 | } 89 | 90 | public static AppDetailBuilder from(AppDetail detail) { 91 | return AppDetail 92 | .builder() 93 | .foundation(detail.getFoundation()) 94 | .organization(detail.getOrganization()) 95 | .space(detail.getSpace()) 96 | .appId(detail.getAppId()) 97 | .appName(detail.getAppName()) 98 | .buildpack(detail.getBuildpack()) 99 | .buildpackVersion(detail.getBuildpackVersion()) 100 | .image(detail.getImage()) 101 | .stack(detail.getStack()) 102 | .runningInstances(detail.getRunningInstances()) 103 | .totalInstances(detail.getTotalInstances()) 104 | .memoryUsed(detail.getMemoryUsed()) 105 | .memoryQuota(detail.getMemoryQuota()) 106 | .diskUsed(detail.getDiskUsed()) 107 | .diskQuota(detail.getDiskQuota()) 108 | .urls(detail.getUrls()) 109 | .lastPushed(detail.getLastPushed()) 110 | .lastEvent(detail.getLastEvent()) 111 | .lastEventActor(detail.getLastEventActor()) 112 | .lastEventTime(detail.getLastEventTime()) 113 | .requestedState(detail.getRequestedState()) 114 | .buildpackReleaseType(detail.getBuildpackReleaseType()) 115 | .buildpackReleaseDate(detail.getBuildpackReleaseDate()) 116 | .buildpackLatestVersion(detail.getBuildpackLatestVersion()) 117 | .buildpackLatestUrl(detail.getBuildpackLatestUrl()); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/AppRelationship.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | 5 | import lombok.AccessLevel; 6 | import lombok.AllArgsConstructor; 7 | import lombok.Builder; 8 | import lombok.EqualsAndHashCode; 9 | import lombok.Getter; 10 | import lombok.NoArgsConstructor; 11 | import lombok.ToString; 12 | 13 | @Builder 14 | @AllArgsConstructor(access=AccessLevel.PACKAGE) 15 | @NoArgsConstructor(access=AccessLevel.PACKAGE) 16 | @Getter 17 | @EqualsAndHashCode 18 | @ToString 19 | public class AppRelationship { 20 | 21 | private String foundation; 22 | private String organization; 23 | private String space; 24 | private String appId; 25 | private String appName; 26 | private String serviceInstanceId; 27 | private String serviceName; 28 | private String servicePlan; 29 | private String serviceType; 30 | 31 | public String toCsv() { 32 | return String.join(",", wrap(getFoundation()), wrap(getOrganization()), wrap(getSpace()), wrap(getAppId()), wrap(getAppName()), 33 | wrap(getServiceInstanceId()), wrap(getServiceName()), wrap(getServicePlan()), wrap(getServiceType())); 34 | } 35 | 36 | private static String wrap(String value) { 37 | return value != null ? StringUtils.wrap(value, '"') : StringUtils.wrap("", '"'); 38 | } 39 | 40 | public static String headers() { 41 | return String.join(",", "foundation", "organization", "space", "application id", 42 | "application name", "service instance id", "service name", "service plan", "service type"); 43 | } 44 | 45 | public static AppRelationshipBuilder from(AppRelationship rel) { 46 | return AppRelationship 47 | .builder() 48 | .foundation(rel.getFoundation()) 49 | .organization(rel.getOrganization()) 50 | .space(rel.getSpace()) 51 | .appId(rel.getAppId()) 52 | .appName(rel.getAppName()) 53 | .serviceInstanceId(rel.getServiceInstanceId()) 54 | .serviceName(rel.getServiceName()) 55 | .servicePlan(rel.getServicePlan()) 56 | .serviceType(rel.getServiceType()); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/ApplicationCounts.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain; 2 | 3 | import java.util.HashMap; 4 | import java.util.List; 5 | import java.util.Map; 6 | import java.util.Map.Entry; 7 | import java.util.Set; 8 | import java.util.stream.Stream; 9 | 10 | import com.fasterxml.jackson.annotation.JsonProperty; 11 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 12 | 13 | import lombok.AccessLevel; 14 | import lombok.AllArgsConstructor; 15 | import lombok.Builder; 16 | import lombok.Getter; 17 | import lombok.NoArgsConstructor; 18 | import lombok.ToString; 19 | import lombok.Builder.Default; 20 | 21 | @Builder 22 | @AllArgsConstructor(access=AccessLevel.PACKAGE) 23 | @NoArgsConstructor(access=AccessLevel.PACKAGE) 24 | @Getter 25 | @ToString 26 | @JsonPropertyOrder({ "by-buildpack", "by-stack", "by-dockerimage", "by-status", 27 | "total-applications", "total-running-application-instances", "total-stopped-application-instances", "total-crashed-application-instances", 28 | "total-application-instances", "total-memory-used-in-gb", "total-disk-used-in-gb", "velocity"}) 29 | public class ApplicationCounts { 30 | 31 | @Default 32 | @JsonProperty("by-buildpack") 33 | private Map byBuildpack = new HashMap<>(); 34 | 35 | @Default 36 | @JsonProperty("by-stack") 37 | private Map byStack = new HashMap<>(); 38 | 39 | @Default 40 | @JsonProperty("by-dockerimage") 41 | private Map byDockerImage = new HashMap<>(); 42 | 43 | @Default 44 | @JsonProperty("by-status") 45 | private Map byStatus = new HashMap<>(); 46 | 47 | @Default 48 | @JsonProperty("total-applications") 49 | private Long totalApplications = 0L; 50 | 51 | @Default 52 | @JsonProperty("total-running-application-instances") 53 | private Long totalRunningApplicationInstances = 0L; 54 | 55 | @Default 56 | @JsonProperty("total-stopped-application-instances") 57 | private Long totalStoppedApplicationInstances = 0L; 58 | 59 | @Default 60 | @JsonProperty("total-crashed-application-instances") 61 | private Long totalCrashedApplicationInstances = 0L; 62 | 63 | @Default 64 | @JsonProperty("total-application-instances") 65 | private Long totalApplicationInstances = 0L; 66 | 67 | @Default 68 | @JsonProperty("total-memory-used-in-gb") 69 | private Double totalMemoryUsed = 0.0; 70 | 71 | @Default 72 | @JsonProperty("total-disk-used-in-gb") 73 | private Double totalDiskUsed = 0.0; 74 | 75 | @Default 76 | @JsonProperty("velocity") 77 | private Map velocity = new HashMap<>(); 78 | 79 | public static ApplicationCounts aggregate(List counts) { 80 | Map byBuildpack = merge(counts.stream().map(c -> c.getByBuildpack().entrySet())); 81 | Map byStack = merge(counts.stream().map(c -> c.getByStack().entrySet())); 82 | Map byDockerImage = merge(counts.stream().map(c -> c.getByDockerImage().entrySet())); 83 | Map byStatus = merge(counts.stream().map(c -> c.getByStatus().entrySet())); 84 | Long totalApplications = counts.stream().mapToLong(c -> c.getTotalApplications()).sum(); 85 | Long totalRunningApplicationInstances = counts.stream().mapToLong(c -> c.getTotalRunningApplicationInstances()).sum(); 86 | Long totalStoppedApplicationInstances = counts.stream().mapToLong(c -> c.getTotalStoppedApplicationInstances()).sum(); 87 | Long totalCrashedApplicationInstances = counts.stream().mapToLong(c -> c.getTotalCrashedApplicationInstances()).sum(); 88 | Long totalApplicationInstances = counts.stream().mapToLong(c -> c.getTotalApplicationInstances()).sum(); 89 | Double totalMemoryUsed = counts.stream().mapToDouble(c -> c.getTotalMemoryUsed()).sum(); 90 | Double totalDiskUsed = counts.stream().mapToDouble(c -> c.getTotalDiskUsed()).sum(); 91 | Map velocity = merge(counts.stream().map(c -> c.getVelocity().entrySet())); 92 | return ApplicationCounts 93 | .builder() 94 | .byBuildpack(byBuildpack) 95 | .byStack(byStack) 96 | .byDockerImage(byDockerImage) 97 | .byStatus(byStatus) 98 | .totalApplications(totalApplications) 99 | .totalRunningApplicationInstances(totalRunningApplicationInstances) 100 | .totalStoppedApplicationInstances(totalStoppedApplicationInstances) 101 | .totalCrashedApplicationInstances(totalCrashedApplicationInstances) 102 | .totalApplicationInstances(totalApplicationInstances) 103 | .totalMemoryUsed(totalMemoryUsed) 104 | .totalDiskUsed(totalDiskUsed) 105 | .velocity(velocity) 106 | .build(); 107 | } 108 | 109 | private static Map merge(Stream>> source) { 110 | Map target = new HashMap<>(); 111 | source.forEach(oe -> oe.forEach(ie -> { 112 | if (target.keySet().contains(ie.getKey())) { 113 | target.put(ie.getKey(), ie.getValue() + target.get(ie.getKey())); 114 | } else { 115 | target.put(ie.getKey(), ie.getValue()); 116 | } 117 | })); 118 | return target; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/Demographic.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 6 | 7 | import lombok.Builder; 8 | import lombok.Builder.Default; 9 | import lombok.Getter; 10 | 11 | @Builder 12 | @Getter 13 | @JsonPropertyOrder({ "foundation", "total-organizations", "total-spaces", "total-user-accounts", "total-service-accounts" }) 14 | public class Demographic { 15 | 16 | @JsonProperty("foundation") 17 | private String foundation; 18 | 19 | @Default 20 | @JsonProperty("total-organizations") 21 | private Long organizations = 0L; 22 | 23 | @Default 24 | @JsonProperty("total-spaces") 25 | private Long spaces = 0L; 26 | 27 | @Default 28 | @JsonProperty("total-user-accounts") 29 | private Long userAccounts = 0L; 30 | 31 | @Default 32 | @JsonProperty("total-service-accounts") 33 | private Long serviceAccounts = 0L; 34 | 35 | @JsonCreator 36 | public Demographic( 37 | @JsonProperty("foundation") String foundation, 38 | @JsonProperty("total-organizations") Long organizations, 39 | @JsonProperty("total-spaces") Long spaces, 40 | @JsonProperty("total-user-accounts") Long userAccounts, 41 | @JsonProperty("total-service-accounts") Long serviceAccounts 42 | ) { 43 | this.foundation = foundation; 44 | this.organizations = organizations; 45 | this.spaces = spaces; 46 | this.userAccounts = userAccounts; 47 | this.serviceAccounts = serviceAccounts; 48 | } 49 | 50 | } -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/Demographics.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain; 2 | 3 | import java.util.HashSet; 4 | import java.util.Set; 5 | 6 | import com.fasterxml.jackson.annotation.JsonCreator; 7 | import com.fasterxml.jackson.annotation.JsonProperty; 8 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 9 | 10 | import lombok.Builder; 11 | import lombok.Builder.Default; 12 | import lombok.Getter; 13 | 14 | @Builder 15 | @Getter 16 | @JsonPropertyOrder({ "demographics", "total-foundations", "total-user-accounts", "total-service-accounts" }) 17 | public class Demographics { 18 | 19 | @Default 20 | @JsonProperty("demographics") 21 | private Set demographics = new HashSet(); 22 | 23 | @Default 24 | @JsonProperty("total-foundations") 25 | private Integer foundations = 0; 26 | 27 | @Default 28 | @JsonProperty("total-user-accounts") 29 | private Long userAccounts = 0L; 30 | 31 | @Default 32 | @JsonProperty("total-service-accounts") 33 | private Long serviceAccounts = 0L; 34 | 35 | @JsonCreator 36 | public Demographics( 37 | @JsonProperty("demographics") Set demographics, 38 | @JsonProperty("total-foundations") Integer foundations, 39 | @JsonProperty("total-user-accounts") Long userAccounts, 40 | @JsonProperty("total-service-accounts") Long serviceAccounts 41 | ) { 42 | this.demographics = demographics; 43 | this.foundations = foundations; 44 | this.userAccounts = userAccounts; 45 | this.serviceAccounts = serviceAccounts; 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/JavaAppDetail.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | 5 | import lombok.AccessLevel; 6 | import lombok.AllArgsConstructor; 7 | import lombok.Builder; 8 | import lombok.EqualsAndHashCode; 9 | import lombok.Getter; 10 | import lombok.NoArgsConstructor; 11 | import lombok.ToString; 12 | 13 | @Builder 14 | @AllArgsConstructor(access=AccessLevel.PACKAGE) 15 | @NoArgsConstructor(access=AccessLevel.PACKAGE) 16 | @Getter 17 | @EqualsAndHashCode 18 | @ToString 19 | public class JavaAppDetail { 20 | 21 | private String foundation; 22 | private String organization; 23 | private String space; 24 | private String appId; 25 | private String appName; 26 | private String dropletId; 27 | private String pomContents; 28 | private String springDependencies; 29 | 30 | public static JavaAppDetailBuilder from(JavaAppDetail detail) { 31 | return JavaAppDetail 32 | .builder() 33 | .foundation(detail.getFoundation()) 34 | .organization(detail.getOrganization()) 35 | .space(detail.getSpace()) 36 | .appId(detail.getAppId()) 37 | .appName(detail.getAppName()) 38 | .dropletId(detail.getDropletId()) 39 | .pomContents(detail.getPomContents()) 40 | .springDependencies(detail.getSpringDependencies()); 41 | } 42 | 43 | public static String headers() { 44 | return String.join(",", "foundation", "organization", "space", "application id", "application name", "droplet id", "pom contents", "spring dependencies" ); 45 | } 46 | 47 | private static String wrap(String value) { 48 | return value != null ? StringUtils.wrap(value, '"') : StringUtils.wrap("", '"'); 49 | } 50 | 51 | public String toCsv() { 52 | return String.join(",", wrap(getFoundation()), wrap(getOrganization()), wrap(getSpace()), wrap(getAppId()), wrap(getAppName()), 53 | wrap(getDropletId()), wrap(getPomContents()), wrap(getSpringDependencies())); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/Organization.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 6 | 7 | import lombok.Builder; 8 | import lombok.EqualsAndHashCode; 9 | import lombok.Getter; 10 | import lombok.ToString; 11 | 12 | @Builder 13 | @Getter 14 | @EqualsAndHashCode 15 | @JsonPropertyOrder({ "foundation", "id", "name"}) 16 | @ToString 17 | public class Organization { 18 | 19 | @JsonProperty("foundation") 20 | private String foundation; 21 | 22 | @JsonProperty("id") 23 | private final String id; 24 | 25 | @JsonProperty("name") 26 | private final String name; 27 | 28 | @JsonCreator 29 | public Organization( 30 | @JsonProperty("foundation") String foundation, 31 | @JsonProperty("id") String id, 32 | @JsonProperty("name") String name) { 33 | this.foundation = foundation; 34 | this.id = id; 35 | this.name = name; 36 | } 37 | 38 | public static OrganizationBuilder from(Organization organization) { 39 | return Organization 40 | .builder() 41 | .foundation(organization.getFoundation()) 42 | .id(organization.getId()) 43 | .name(organization.getName()); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/ServiceInstanceCounts.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain; 2 | 3 | import java.util.HashMap; 4 | import java.util.List; 5 | import java.util.Map; 6 | import java.util.Set; 7 | import java.util.Map.Entry; 8 | import java.util.stream.Stream; 9 | 10 | import com.fasterxml.jackson.annotation.JsonProperty; 11 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 12 | 13 | import lombok.AccessLevel; 14 | import lombok.AllArgsConstructor; 15 | import lombok.Builder; 16 | import lombok.Getter; 17 | import lombok.NoArgsConstructor; 18 | import lombok.ToString; 19 | import lombok.Builder.Default; 20 | 21 | @Builder 22 | @AllArgsConstructor(access=AccessLevel.PACKAGE) 23 | @NoArgsConstructor(access=AccessLevel.PACKAGE) 24 | @Getter 25 | @ToString 26 | @JsonPropertyOrder({ "by-service", "by-service-and-plan", "total-service-instances", "velocity" }) 27 | public class ServiceInstanceCounts { 28 | 29 | @Default 30 | @JsonProperty("by-service") 31 | private Map byService = new HashMap<>(); 32 | 33 | @Default 34 | @JsonProperty("by-service-and-plan") 35 | private Map byServiceAndPlan = new HashMap<>(); 36 | 37 | @Default 38 | @JsonProperty("total-service-instances") 39 | private Long totalServiceInstances = 0L; 40 | 41 | @Default 42 | @JsonProperty("velocity") 43 | private Map velocity = new HashMap<>(); 44 | 45 | public static ServiceInstanceCounts aggregate(List counts) { 46 | Map byService = merge(counts.stream().map(c -> c.getByService().entrySet())); 47 | Map byServiceAndPlan = merge(counts.stream().map(c -> c.getByServiceAndPlan().entrySet())); 48 | Long totalServiceInstances = counts.stream().mapToLong(c -> c.getTotalServiceInstances()).sum(); 49 | Map velocity = merge(counts.stream().map(c -> c.getVelocity().entrySet())); 50 | return ServiceInstanceCounts 51 | .builder() 52 | .byService(byService) 53 | .byServiceAndPlan(byServiceAndPlan) 54 | .totalServiceInstances(totalServiceInstances) 55 | .velocity(velocity) 56 | .build(); 57 | } 58 | 59 | private static Map merge(Stream>> source) { 60 | Map target = new HashMap<>(); 61 | source.forEach(oe -> oe.forEach(ie -> { 62 | if (target.keySet().contains(ie.getKey())) { 63 | target.put(ie.getKey(), ie.getValue() + target.get(ie.getKey())); 64 | } else { 65 | target.put(ie.getKey(), ie.getValue()); 66 | } 67 | })); 68 | return target; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/ServiceInstanceDetail.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain; 2 | 3 | import java.time.LocalDateTime; 4 | import java.util.Collections; 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | import org.apache.commons.lang3.StringUtils; 9 | 10 | import lombok.AccessLevel; 11 | import lombok.AllArgsConstructor; 12 | import lombok.Builder; 13 | import lombok.EqualsAndHashCode; 14 | import lombok.Getter; 15 | import lombok.NoArgsConstructor; 16 | import lombok.ToString; 17 | import lombok.Builder.Default; 18 | 19 | @Builder 20 | @AllArgsConstructor(access=AccessLevel.PACKAGE) 21 | @NoArgsConstructor(access=AccessLevel.PACKAGE) 22 | @Getter 23 | @EqualsAndHashCode 24 | @ToString 25 | public class ServiceInstanceDetail { 26 | 27 | private String foundation; 28 | private String organization; 29 | private String space; 30 | private String serviceInstanceId; 31 | private String name; 32 | private String service; 33 | private String description; 34 | private String plan; 35 | private String type; 36 | @Default 37 | private List applications = new ArrayList<>(); 38 | private String lastOperation; 39 | private LocalDateTime lastUpdated; 40 | private String dashboardUrl; 41 | private String requestedState; 42 | 43 | public String toCsv() { 44 | return String.join(",", wrap(getFoundation()), wrap(getOrganization()), wrap(getSpace()), wrap(getServiceInstanceId()), wrap(getName()), 45 | wrap(getService()), wrap(getDescription()), wrap(getPlan()), wrap(getType()), 46 | wrap(String.join(",", getApplications() != null ? getApplications(): Collections.emptyList())), wrap(getLastOperation()), 47 | wrap(getLastUpdated() != null ? getLastUpdated().toString() : ""), wrap(getDashboardUrl()), 48 | wrap(getRequestedState())); 49 | } 50 | 51 | private static String wrap(String value) { 52 | return value != null ? StringUtils.wrap(value, '"') : StringUtils.wrap("", '"'); 53 | } 54 | 55 | public static String headers() { 56 | return String.join(",", "foundation", "organization", "space", "service instance id", 57 | "name", "service", "description", "plan", "type", "bound applications", "last operation", "last updated", "dashboard url", "requested state"); 58 | } 59 | 60 | public static ServiceInstanceDetailBuilder from(ServiceInstanceDetail detail) { 61 | return ServiceInstanceDetail 62 | .builder() 63 | .foundation(detail.getFoundation()) 64 | .organization(detail.getOrganization()) 65 | .space(detail.getSpace()) 66 | .serviceInstanceId(detail.getServiceInstanceId()) 67 | .name(detail.getName()) 68 | .service(detail.getService()) 69 | .description(detail.getDescription()) 70 | .plan(detail.getPlan()) 71 | .type(detail.getType()) 72 | .applications(detail.getApplications()) 73 | .lastOperation(detail.getLastOperation()) 74 | .lastUpdated(detail.getLastUpdated()) 75 | .dashboardUrl(detail.getDashboardUrl()) 76 | .requestedState(detail.getRequestedState()); 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/SnapshotDetail.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain; 2 | 3 | import java.util.ArrayList; 4 | import java.util.HashSet; 5 | import java.util.List; 6 | import java.util.Set; 7 | 8 | import com.fasterxml.jackson.annotation.JsonCreator; 9 | import com.fasterxml.jackson.annotation.JsonProperty; 10 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 11 | 12 | import lombok.Builder; 13 | import lombok.Builder.Default; 14 | import lombok.Getter; 15 | import lombok.ToString; 16 | 17 | @Builder 18 | @Getter 19 | @ToString 20 | @JsonPropertyOrder({ "applications", "service-instances", "application-relationships", "user-accounts", "service-accounts" }) 21 | public class SnapshotDetail { 22 | 23 | @Default 24 | @JsonProperty("applications") 25 | private List applications = new ArrayList<>(); 26 | 27 | @Default 28 | @JsonProperty("service-instances") 29 | private List serviceInstances = new ArrayList<>(); 30 | 31 | @Default 32 | @JsonProperty("application-relationships") 33 | private List applicationRelationships = new ArrayList<>(); 34 | 35 | @Default 36 | @JsonProperty("user-accounts") 37 | private Set userAccounts = new HashSet<>(); 38 | 39 | @Default 40 | @JsonProperty("service-accounts") 41 | private Set serviceAccounts = new HashSet<>(); 42 | 43 | @JsonCreator 44 | public SnapshotDetail( 45 | @JsonProperty("applications") List applications, 46 | @JsonProperty("service-instances") List serviceInstances, 47 | @JsonProperty("application-relationships") List applicationRelationships, 48 | @JsonProperty("user-accounts") Set userAccounts, 49 | @JsonProperty("service-accounts") Set serviceAccounts) { 50 | this.applications = applications; 51 | this.serviceInstances = serviceInstances; 52 | this.applicationRelationships = applicationRelationships; 53 | this.userAccounts = userAccounts; 54 | this.serviceAccounts = serviceAccounts; 55 | } 56 | 57 | } -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/SnapshotSummary.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 6 | 7 | import lombok.Builder; 8 | import lombok.Builder.Default; 9 | import lombok.Getter; 10 | import lombok.ToString; 11 | 12 | @Builder 13 | @Getter 14 | @ToString 15 | @JsonPropertyOrder({ "application-counts", "service-instance-counts" }) 16 | public class SnapshotSummary { 17 | 18 | @Default 19 | @JsonProperty("application-counts") 20 | private ApplicationCounts applicationCounts = ApplicationCounts.builder().build(); 21 | 22 | @Default 23 | @JsonProperty("service-instance-counts") 24 | private ServiceInstanceCounts serviceInstanceCounts = ServiceInstanceCounts.builder().build(); 25 | 26 | @JsonCreator 27 | public SnapshotSummary( 28 | @JsonProperty("application-counts") ApplicationCounts applicationCounts, 29 | @JsonProperty("service-instance-counts") ServiceInstanceCounts serviceInstanceCounts) { 30 | this.applicationCounts = applicationCounts; 31 | this.serviceInstanceCounts = serviceInstanceCounts; 32 | } 33 | 34 | } -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/Space.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 6 | 7 | import lombok.Builder; 8 | import lombok.EqualsAndHashCode; 9 | import lombok.Getter; 10 | import lombok.ToString; 11 | 12 | @Builder 13 | @Getter 14 | @JsonPropertyOrder({ "foundation", "organization-id", "organization-name", "space-id", "space-name" }) 15 | @EqualsAndHashCode 16 | @ToString 17 | public class Space { 18 | 19 | @JsonProperty("foundation") 20 | private String foundation; 21 | 22 | @JsonProperty("organization-id") 23 | private final String organizationId; 24 | 25 | @JsonProperty("organization-name") 26 | private final String organizationName; 27 | 28 | @JsonProperty("space-id") 29 | private final String spaceId; 30 | 31 | @JsonProperty("space-name") 32 | private final String spaceName; 33 | 34 | @JsonCreator 35 | Space( 36 | @JsonProperty("foundation") String foundation, 37 | @JsonProperty("organization-id") String organizationId, 38 | @JsonProperty("organization-name") String organizationName, 39 | @JsonProperty("space-id") String spaceId, 40 | @JsonProperty("space-name") String spaceName) { 41 | this.foundation = foundation; 42 | this.organizationId = organizationId; 43 | this.organizationName = organizationName; 44 | this.spaceId = spaceId; 45 | this.spaceName = spaceName; 46 | } 47 | 48 | public static SpaceBuilder from(Space space) { 49 | return Space 50 | .builder() 51 | .foundation(space.getFoundation()) 52 | .organizationId(space.getOrganizationId()) 53 | .organizationName(space.getOrganizationName()) 54 | .spaceId(space.getSpaceId()) 55 | .spaceName(space.getSpaceName()); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/SpaceUsers.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain; 2 | 3 | import java.util.ArrayList; 4 | import java.util.HashSet; 5 | import java.util.List; 6 | import java.util.Set; 7 | 8 | import com.fasterxml.jackson.annotation.JsonCreator; 9 | import com.fasterxml.jackson.annotation.JsonProperty; 10 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 11 | 12 | import lombok.Builder; 13 | import lombok.Builder.Default; 14 | import lombok.EqualsAndHashCode; 15 | import lombok.Getter; 16 | import lombok.ToString; 17 | 18 | @Builder 19 | @Getter 20 | @EqualsAndHashCode 21 | @ToString 22 | @JsonPropertyOrder({"foundation", "organization", "space", "auditors", "developers", "managers", "users", "user-count"}) 23 | public class SpaceUsers { 24 | 25 | @JsonProperty("foundation") 26 | private String foundation; 27 | 28 | @JsonProperty("organization") 29 | private String organization; 30 | 31 | @JsonProperty("space") 32 | private String space; 33 | 34 | @Default 35 | @JsonProperty("auditors") 36 | private List auditors = new ArrayList<>(); 37 | 38 | @Default 39 | @JsonProperty("developers") 40 | private List developers = new ArrayList<>(); 41 | 42 | @Default 43 | @JsonProperty("managers") 44 | private List managers = new ArrayList<>(); 45 | 46 | @Default 47 | @JsonProperty("users") 48 | private Set users = new HashSet<>(); 49 | 50 | @Default 51 | @JsonProperty("user-count") 52 | private Integer userCount = 0; 53 | 54 | @JsonCreator 55 | public SpaceUsers( 56 | @JsonProperty("foundation") String foundation, 57 | @JsonProperty("organization") String organization, 58 | @JsonProperty("space") String space, 59 | @JsonProperty("auditors") List auditors, 60 | @JsonProperty("developers") List developers, 61 | @JsonProperty("managers") List managers, 62 | @JsonProperty("users") Set users, 63 | @JsonProperty("user-count") Integer userCount 64 | ) { 65 | this.foundation = foundation; 66 | this.organization = organization; 67 | this.space = space; 68 | this.auditors = auditors; 69 | this.developers = developers; 70 | this.managers = managers; 71 | this.users = users; 72 | this.userCount = userCount; 73 | } 74 | 75 | public static SpaceUsersBuilder from(SpaceUsers users) { 76 | return SpaceUsers 77 | .builder() 78 | .organization(users.getOrganization()) 79 | .space(users.getSpace()) 80 | .foundation(users.getFoundation()) 81 | .auditors(users.getAuditors()) 82 | .developers(users.getDevelopers()) 83 | .managers(users.getManagers()) 84 | .users(users.getUsers()) 85 | .userCount(users.getUserCount()); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/TimeKeeper.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain; 2 | 3 | import java.time.LocalDateTime; 4 | 5 | import org.apache.commons.lang3.StringUtils; 6 | 7 | import com.fasterxml.jackson.annotation.JsonCreator; 8 | import com.fasterxml.jackson.annotation.JsonProperty; 9 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 10 | 11 | import lombok.AccessLevel; 12 | import lombok.Builder; 13 | import lombok.EqualsAndHashCode; 14 | import lombok.Getter; 15 | import lombok.NoArgsConstructor; 16 | import lombok.ToString; 17 | 18 | @Builder 19 | @NoArgsConstructor(access=AccessLevel.PACKAGE) 20 | @Getter 21 | @EqualsAndHashCode 22 | @ToString 23 | @JsonPropertyOrder({ "foundation", "collection-date-time" }) 24 | public class TimeKeeper { 25 | 26 | @JsonProperty("foundation") 27 | private String foundation; 28 | 29 | @JsonProperty("collection-date-time") 30 | private LocalDateTime collectionDateTime; 31 | 32 | @JsonCreator 33 | TimeKeeper(@JsonProperty("foundation") String foundation, 34 | @JsonProperty("collection-date-time") LocalDateTime collectionDateTime) { 35 | this.foundation = foundation; 36 | this.collectionDateTime = collectionDateTime; 37 | } 38 | 39 | public String toCsv() { 40 | return String.join(",", wrap(getFoundation()), wrap(getCollectionDateTime() != null ? getCollectionDateTime().toString() : "")); 41 | } 42 | 43 | private static String wrap(String value) { 44 | return value != null ? StringUtils.wrap(value, '"') : StringUtils.wrap("", '"'); 45 | } 46 | 47 | public static String headers() { 48 | return String.join(",", "foundation", "collection date/time"); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/TimeKeepers.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain; 2 | 3 | import java.util.HashSet; 4 | import java.util.Set; 5 | 6 | import com.fasterxml.jackson.annotation.JsonCreator; 7 | import com.fasterxml.jackson.annotation.JsonProperty; 8 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 9 | 10 | import lombok.Builder; 11 | import lombok.Builder.Default; 12 | import lombok.Getter; 13 | import lombok.ToString; 14 | 15 | @Builder 16 | @Getter 17 | @ToString 18 | @JsonPropertyOrder({ "time-keepers" }) 19 | public class TimeKeepers { 20 | 21 | @Default 22 | @JsonProperty("time-keepers") 23 | private Set timeKeepers = new HashSet<>(); 24 | 25 | @JsonCreator 26 | public TimeKeepers( 27 | @JsonProperty("time-keepers") Set timeKeepers) { 28 | this.timeKeepers = timeKeepers; 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/accounting/application/AppUsageMonthly.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain.accounting.application; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonIgnore; 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 7 | 8 | import lombok.Builder; 9 | import lombok.Builder.Default; 10 | import lombok.Getter; 11 | 12 | @Builder 13 | @Getter 14 | @JsonPropertyOrder({"month", "year", "average_app_instances","maximum_app_instances", "app_instance_hours"}) 15 | public class AppUsageMonthly { 16 | 17 | @JsonProperty("month") 18 | private Integer month; 19 | 20 | @JsonProperty("year") 21 | private Integer year; 22 | 23 | @Default 24 | @JsonProperty("average_app_instances") 25 | private Double averageAppInstances = 0.0; 26 | 27 | @Default 28 | @JsonProperty("maximum_app_instances") 29 | private Integer maximumAppInstances = 0; 30 | 31 | @Default 32 | @JsonProperty("app_instance_hours") 33 | private Double appInstanceHours = 0.0; 34 | 35 | @JsonCreator 36 | public AppUsageMonthly( 37 | @JsonProperty("month") Integer month, 38 | @JsonProperty("year") Integer year, 39 | @JsonProperty("average_app_instances") Double averageAppInstances, 40 | @JsonProperty("maximum_app_instances") Integer maximumAppInstances, 41 | @JsonProperty("app_instance_hours") Double appInstanceHours) { 42 | this.month = month; 43 | this.year = year; 44 | this.averageAppInstances = averageAppInstances; 45 | this.maximumAppInstances = maximumAppInstances; 46 | this.appInstanceHours = appInstanceHours; 47 | } 48 | 49 | 50 | @JsonIgnore 51 | public AppUsageMonthly combine(AppUsageMonthly usage) { 52 | AppUsageMonthly result = null; 53 | if (usage == null) { 54 | result = this; 55 | } else if (usage.getMonth().equals(month) && usage.getYear().equals(year)) { 56 | result = 57 | AppUsageMonthly 58 | .builder() 59 | .year(usage.getYear()) 60 | .month(usage.getMonth()) 61 | .appInstanceHours(this.appInstanceHours + usage.getAppInstanceHours()) 62 | .averageAppInstances(this.averageAppInstances + usage.getAverageAppInstances()) 63 | .maximumAppInstances(this.maximumAppInstances + usage.getMaximumAppInstances()) 64 | .build(); 65 | } else { 66 | result = usage; 67 | } 68 | return result; 69 | } 70 | 71 | @JsonIgnore 72 | public String getYearAndMonth() { 73 | return String.join("-", String.valueOf(year), String.format("%02d", month)); 74 | } 75 | 76 | } -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/accounting/application/AppUsageReport.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain.accounting.application; 2 | 3 | import java.time.LocalDateTime; 4 | import java.util.ArrayList; 5 | import java.util.Comparator; 6 | import java.util.HashMap; 7 | import java.util.List; 8 | import java.util.Map; 9 | 10 | import com.fasterxml.jackson.annotation.JsonCreator; 11 | import com.fasterxml.jackson.annotation.JsonProperty; 12 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 13 | 14 | import lombok.Builder; 15 | import lombok.Builder.Default; 16 | import lombok.Getter; 17 | 18 | @Builder 19 | @Getter 20 | @JsonPropertyOrder({"report_time", "monthly_reports", "yearly_reports"}) 21 | public class AppUsageReport { 22 | 23 | @JsonProperty("report_time") 24 | private String reportTime; 25 | 26 | @Default 27 | @JsonProperty("monthly_reports") 28 | private List monthlyReports = new ArrayList<>(); 29 | 30 | @Default 31 | @JsonProperty("yearly_reports") 32 | private List yearlyReports = new ArrayList<>(); 33 | 34 | @JsonCreator 35 | public AppUsageReport( 36 | @JsonProperty("report_time") String reportTime, 37 | @JsonProperty("monthly_reports") List monthlyReports, 38 | @JsonProperty("yearly_reports") List yearlyReports) { 39 | this.reportTime = reportTime; 40 | this.monthlyReports = monthlyReports; 41 | this.yearlyReports = yearlyReports; 42 | } 43 | 44 | 45 | public static AppUsageReport aggregate(List source) { 46 | AppUsageReportBuilder report = AppUsageReport.builder(); 47 | Map monthlyReports = new HashMap<>(); 48 | Map yearlyReports = new HashMap<>(); 49 | report.reportTime(LocalDateTime.now().toString()); 50 | source.forEach(aur -> { 51 | for (AppUsageMonthly smr: aur.getMonthlyReports()) { 52 | if (monthlyReports.isEmpty()) { 53 | monthlyReports.put(smr.getYearAndMonth(), smr); 54 | } else { 55 | AppUsageMonthly existing = monthlyReports.get(smr.getYearAndMonth()); 56 | monthlyReports.put(smr.getYearAndMonth(), smr.combine(existing)); 57 | } 58 | } 59 | for (AppUsageYearly syr: aur.getYearlyReports()) { 60 | if (yearlyReports.isEmpty()) { 61 | yearlyReports.put(syr.getYear(), syr); 62 | } else { 63 | AppUsageYearly existing = yearlyReports.get(syr.getYear()); 64 | yearlyReports.put(syr.getYear(), syr.combine(existing)); 65 | } 66 | } 67 | }); 68 | List sortedMonthlyReports = new ArrayList<>(); 69 | sortedMonthlyReports.addAll(monthlyReports.values()); 70 | sortedMonthlyReports.sort(Comparator.comparing(AppUsageMonthly::getYearAndMonth)); 71 | report.monthlyReports(sortedMonthlyReports); 72 | List sortedYearlyReports = new ArrayList<>(); 73 | sortedYearlyReports.addAll(yearlyReports.values()); 74 | sortedYearlyReports.sort(Comparator.comparing(AppUsageYearly::getYear)); 75 | report.yearlyReports(sortedYearlyReports); 76 | return report.build(); 77 | } 78 | } -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/accounting/application/AppUsageYearly.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain.accounting.application; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonIgnore; 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 7 | 8 | import lombok.Builder; 9 | import lombok.Builder.Default; 10 | import lombok.Getter; 11 | 12 | @Builder 13 | @Getter 14 | @JsonPropertyOrder({"year", "average_app_instances", "maximum_app_instances", "app_instance_hours"}) 15 | public class AppUsageYearly { 16 | 17 | @JsonProperty("year") 18 | private Integer year; 19 | 20 | @Default 21 | @JsonProperty("average_app_instances") 22 | private Double averageAppInstances = 0.0; 23 | 24 | @Default 25 | @JsonProperty("maximum_app_instances") 26 | private Integer maximumAppInstances = 0; 27 | 28 | @Default 29 | @JsonProperty("app_instance_hours") 30 | private Double appInstanceHours = 0.0; 31 | 32 | @JsonCreator 33 | public AppUsageYearly( 34 | @JsonProperty("year") Integer year, 35 | @JsonProperty("average_app_instances") Double averageAppInstances, 36 | @JsonProperty("maximum_app_instances") Integer maximumAppInstances, 37 | @JsonProperty("app_instance_hours") Double appInstanceHours) { 38 | this.year = year; 39 | this.averageAppInstances = averageAppInstances; 40 | this.maximumAppInstances = maximumAppInstances; 41 | this.appInstanceHours = appInstanceHours; 42 | } 43 | 44 | @JsonIgnore 45 | public AppUsageYearly combine(AppUsageYearly usage) { 46 | AppUsageYearly result = null; 47 | if (usage == null) { 48 | result = this; 49 | } else if (usage.getYear().equals(year)) { 50 | result = 51 | AppUsageYearly 52 | .builder() 53 | .year(usage.getYear()) 54 | .appInstanceHours(this.appInstanceHours + usage.getAppInstanceHours()) 55 | .averageAppInstances(this.averageAppInstances + usage.getAverageAppInstances()) 56 | .maximumAppInstances(this.maximumAppInstances + usage.getMaximumAppInstances()) 57 | .build(); 58 | } else { 59 | result = usage; 60 | } 61 | return result; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/accounting/service/ServicePlanUsageMonthly.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain.accounting.service; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import com.fasterxml.jackson.annotation.JsonCreator; 7 | import com.fasterxml.jackson.annotation.JsonIgnore; 8 | import com.fasterxml.jackson.annotation.JsonProperty; 9 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 10 | 11 | import lombok.Builder; 12 | import lombok.Builder.Default; 13 | import lombok.Getter; 14 | 15 | @Builder 16 | @Getter 17 | @JsonPropertyOrder({ "usages", "service_plan_name", "service_plan_guid"}) 18 | public class ServicePlanUsageMonthly { 19 | 20 | @Default 21 | @JsonProperty("usages") 22 | public List usages = new ArrayList<>(); 23 | 24 | @JsonProperty("service_plan_name") 25 | public String servicePlanName; 26 | 27 | @JsonProperty("service_plan_guid") 28 | public String servicePlanGuid; 29 | 30 | @JsonCreator 31 | public ServicePlanUsageMonthly( 32 | @JsonProperty("usages") List usages, 33 | @JsonProperty("service_plan_name") String servicePlanName, 34 | @JsonProperty("service_plan_guid") String servicePlanGuid) { 35 | this.usages = usages; 36 | this.servicePlanName = servicePlanName; 37 | this.servicePlanGuid = servicePlanGuid; 38 | } 39 | 40 | @JsonIgnore 41 | public ServicePlanUsageMonthly combine(ServicePlanUsageMonthly usage) { 42 | ServicePlanUsageMonthly result = null; 43 | if (usage == null) { 44 | result = this; 45 | } else if (usage.getServicePlanName().equals(servicePlanName)) { 46 | List u = new ArrayList<>(); 47 | for (ServiceUsageMonthly su: usage.getUsages()) { 48 | for (ServiceUsageMonthly suu: usages) { 49 | u.add(suu.combine(su)); 50 | } 51 | } 52 | String newServicePlanGuid = usage.getServicePlanGuid(); 53 | if (!usage.getServicePlanGuid().contains(this.servicePlanGuid)) { 54 | newServicePlanGuid = String.join(",", this.servicePlanGuid, usage.getServicePlanGuid()); 55 | } 56 | result = 57 | ServicePlanUsageMonthly 58 | .builder() 59 | .servicePlanGuid(newServicePlanGuid) 60 | .servicePlanName(usage.getServicePlanName()) 61 | .usages(u) 62 | .build(); 63 | } else { 64 | result = usage; 65 | } 66 | return result; 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/accounting/service/ServicePlanUsageYearly.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain.accounting.service; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonIgnore; 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 7 | 8 | import lombok.Builder; 9 | import lombok.Builder.Default; 10 | import lombok.Getter; 11 | 12 | @Builder 13 | @Getter 14 | @JsonPropertyOrder({ "service_plan_name", "service_plan_guid", "year", "duration_in_hours", "maximum_instances", "average_instances"}) 15 | public class ServicePlanUsageYearly { 16 | 17 | @JsonProperty("service_plan_name") 18 | public String servicePlanName; 19 | 20 | @JsonProperty("service_plan_guid") 21 | public String servicePlanGuid; 22 | 23 | @JsonProperty("year") 24 | public Integer year; 25 | 26 | @Default 27 | @JsonProperty("duration_in_hours") 28 | public Double durationInHours = 0.0; 29 | 30 | @Default 31 | @JsonProperty("maximum_instances") 32 | public Integer maximumInstances = 0; 33 | 34 | @Default 35 | @JsonProperty("average_instances") 36 | public Double averageInstances = 0.0; 37 | 38 | @JsonCreator 39 | public ServicePlanUsageYearly( 40 | @JsonProperty("service_plan_name") String servicePlanName, 41 | @JsonProperty("service_plan_guid") String servicePlanGuid, 42 | @JsonProperty("year") Integer year, 43 | @JsonProperty("duration_in_hours") Double durationInHours, 44 | @JsonProperty("maximum_instances") Integer maximumInstances, 45 | @JsonProperty("average_instances") Double averageInstances) { 46 | this.servicePlanName = servicePlanName; 47 | this.servicePlanGuid = servicePlanGuid; 48 | this.year = year; 49 | this.durationInHours = durationInHours; 50 | this.maximumInstances = maximumInstances; 51 | this.averageInstances = averageInstances; 52 | } 53 | 54 | @JsonIgnore 55 | public ServicePlanUsageYearly combine(ServicePlanUsageYearly usage) { 56 | ServicePlanUsageYearly result = null; 57 | if (usage == null) { 58 | result = this; 59 | } else if (usage.getYear().equals(year) && usage.getServicePlanName().equals(servicePlanName)) { 60 | String newServicePlanGuid = usage.getServicePlanGuid(); 61 | if (!usage.getServicePlanGuid().contains(this.servicePlanGuid)) { 62 | newServicePlanGuid = String.join(",", this.servicePlanGuid, usage.getServicePlanGuid()); 63 | } 64 | result = 65 | ServicePlanUsageYearly 66 | .builder() 67 | .year(usage.getYear()) 68 | .servicePlanGuid(newServicePlanGuid) 69 | .servicePlanName(usage.getServicePlanName()) 70 | .durationInHours(this.durationInHours + usage.getDurationInHours()) 71 | .averageInstances(this.averageInstances + usage.getAverageInstances()) 72 | .maximumInstances(this.maximumInstances + usage.getMaximumInstances()) 73 | .build(); 74 | } else { 75 | result = usage; 76 | } 77 | return result; 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/accounting/service/ServiceUsageMonthly.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain.accounting.service; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonIgnore; 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 7 | 8 | import lombok.Builder; 9 | import lombok.Builder.Default; 10 | import lombok.Getter; 11 | 12 | @Builder 13 | @Getter 14 | @JsonPropertyOrder({"month", "year", "duration_in_hours", "average_instances", "maximum_instances"}) 15 | public class ServiceUsageMonthly { 16 | 17 | @JsonProperty("month") 18 | public Integer month; 19 | 20 | @JsonProperty("year") 21 | public Integer year; 22 | 23 | @Default 24 | @JsonProperty("duration_in_hours") 25 | public Double durationInHours = 0.0; 26 | 27 | @Default 28 | @JsonProperty("average_instances") 29 | public Double averageInstances = 0.0; 30 | 31 | @Default 32 | @JsonProperty("maximum_instances") 33 | public Integer maximumInstances = 0; 34 | 35 | @JsonCreator 36 | public ServiceUsageMonthly( 37 | @JsonProperty("month") Integer month, 38 | @JsonProperty("year") Integer year, 39 | @JsonProperty("duration_in_hours") Double durationInHours, 40 | @JsonProperty("average_instances") Double averageInstances, 41 | @JsonProperty("maximum_instances") Integer maximumInstances) { 42 | this.month = month; 43 | this.year = year; 44 | this.durationInHours = durationInHours; 45 | this.averageInstances = averageInstances; 46 | this.maximumInstances = maximumInstances; 47 | } 48 | 49 | @JsonIgnore 50 | public ServiceUsageMonthly combine(ServiceUsageMonthly usage) { 51 | ServiceUsageMonthly result = null; 52 | if (usage == null) { 53 | result = this; 54 | } else if (usage.getYear().equals(year) && usage.getMonth().equals(month)) { 55 | result = 56 | ServiceUsageMonthly 57 | .builder() 58 | .month(usage.getMonth()) 59 | .year(usage.getYear()) 60 | .durationInHours(this.durationInHours + usage.getDurationInHours()) 61 | .averageInstances(this.averageInstances + usage.getAverageInstances()) 62 | .maximumInstances(this.maximumInstances + usage.getMaximumInstances()) 63 | .build(); 64 | } else { 65 | result = usage; 66 | } 67 | return result; 68 | } 69 | 70 | @JsonIgnore 71 | public String getYearAndMonth() { 72 | return String.join("-", String.valueOf(year), String.format("%02d", month)); 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/accounting/service/ServiceUsageMonthlyAggregate.java: -------------------------------------------------------------------------------- 1 | 2 | package org.cftoolsuite.cfapp.domain.accounting.service; 3 | 4 | import java.util.ArrayList; 5 | import java.util.Comparator; 6 | import java.util.List; 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | 10 | import com.fasterxml.jackson.annotation.JsonCreator; 11 | import com.fasterxml.jackson.annotation.JsonIgnore; 12 | import com.fasterxml.jackson.annotation.JsonProperty; 13 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 14 | 15 | import lombok.Builder; 16 | import lombok.Getter; 17 | import lombok.Builder.Default; 18 | 19 | @Builder 20 | @Getter 21 | @JsonPropertyOrder({"service_name", "service_guid", "usages", "plans"}) 22 | public class ServiceUsageMonthlyAggregate { 23 | 24 | @JsonProperty("service_name") 25 | public String serviceName; 26 | 27 | @JsonProperty("service_guid") 28 | public String serviceGuid; 29 | 30 | @Default 31 | @JsonProperty("usages") 32 | public List usages = new ArrayList<>(); 33 | 34 | @Default 35 | @JsonProperty("plans") 36 | public List plans = new ArrayList<>(); 37 | 38 | @JsonCreator 39 | public ServiceUsageMonthlyAggregate( 40 | @JsonProperty("service_name") String serviceName, 41 | @JsonProperty("service_guid") String serviceGuid, 42 | @JsonProperty("usages") List usages, 43 | @JsonProperty("plans") List plans) { 44 | this.serviceName = serviceName; 45 | this.serviceGuid = serviceGuid; 46 | this.usages = usages; 47 | this.plans = plans; 48 | } 49 | 50 | @JsonIgnore 51 | public ServiceUsageMonthlyAggregate combine(ServiceUsageMonthlyAggregate usage) { 52 | ServiceUsageMonthlyAggregate result = null; 53 | if (usage == null) { 54 | result = this; 55 | } else if (usage.getServiceName().equals(serviceName)) { 56 | Map monthlyUsage = new HashMap<>(); 57 | Map monthlyPlans = new HashMap<>(); 58 | usage.getUsages().forEach(su -> { 59 | for (ServiceUsageMonthly suu: usages) { 60 | if (monthlyUsage.isEmpty()) { 61 | monthlyUsage.put(suu.getYearAndMonth(), suu); 62 | } else { 63 | ServiceUsageMonthly existing = monthlyUsage.get(suu.getYearAndMonth()); 64 | monthlyUsage.put(suu.getYearAndMonth(), suu.combine(existing)); 65 | } 66 | } 67 | }); 68 | usage.getPlans().forEach(pu -> { 69 | for (ServicePlanUsageMonthly spu: plans) { 70 | if (monthlyPlans.isEmpty()) { 71 | monthlyPlans.put(spu.getServicePlanName(), spu); 72 | } else { 73 | ServicePlanUsageMonthly existing = monthlyPlans.get(spu.getServicePlanName()); 74 | monthlyPlans.put(spu.getServicePlanName(), spu.combine(existing)); 75 | } 76 | } 77 | }); 78 | List sortedMonthlyUsage = new ArrayList<>(); 79 | sortedMonthlyUsage.addAll(monthlyUsage.values()); 80 | sortedMonthlyUsage.sort(Comparator.comparing(ServiceUsageMonthly::getYearAndMonth)); 81 | List sortedMonthlyPlans = new ArrayList<>(); 82 | sortedMonthlyPlans.addAll(monthlyPlans.values()); 83 | sortedMonthlyPlans.sort(Comparator.comparing(ServicePlanUsageMonthly::getServicePlanName)); 84 | String newServiceGuid = usage.getServiceGuid(); 85 | if (!usage.getServiceGuid().contains(this.serviceGuid)) { 86 | newServiceGuid = String.join(",", this.serviceGuid, usage.getServiceGuid()); 87 | } 88 | result = 89 | ServiceUsageMonthlyAggregate 90 | .builder() 91 | .serviceGuid(newServiceGuid) 92 | .serviceName(usage.getServiceName()) 93 | .usages(sortedMonthlyUsage) 94 | .plans(sortedMonthlyPlans) 95 | .build(); 96 | } else { 97 | result = usage; 98 | } 99 | return result; 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/accounting/service/ServiceUsageReport.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain.accounting.service; 2 | 3 | import java.time.LocalDateTime; 4 | import java.util.ArrayList; 5 | import java.util.Comparator; 6 | import java.util.HashMap; 7 | import java.util.List; 8 | import java.util.Map; 9 | 10 | import com.fasterxml.jackson.annotation.JsonCreator; 11 | import com.fasterxml.jackson.annotation.JsonProperty; 12 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 13 | 14 | import lombok.Builder; 15 | import lombok.Builder.Default; 16 | import lombok.Getter; 17 | 18 | @Builder 19 | @Getter 20 | @JsonPropertyOrder({"report_time", "monthly_service_reports", "yearly_service_reports"}) 21 | public class ServiceUsageReport { 22 | 23 | @JsonProperty("report_time") 24 | public String reportTime; 25 | 26 | @Default 27 | @JsonProperty("monthly_service_reports") 28 | public List monthlyServiceReports = new ArrayList<>(); 29 | 30 | @Default 31 | @JsonProperty("yearly_service_report") 32 | public List yearlyServiceReport = new ArrayList<>(); 33 | 34 | @JsonCreator 35 | public ServiceUsageReport( 36 | @JsonProperty("report_time") String reportTime, 37 | @JsonProperty("monthly_service_reports") List monthlyServiceReports, 38 | @JsonProperty("yearly_service_report") List yearlyServiceReport) { 39 | this.reportTime = reportTime; 40 | this.monthlyServiceReports = monthlyServiceReports; 41 | this.yearlyServiceReport = yearlyServiceReport; 42 | } 43 | 44 | 45 | public static ServiceUsageReport aggregate(List source) { 46 | ServiceUsageReportBuilder report = ServiceUsageReport.builder(); 47 | Map monthlyReports = new HashMap<>(); 48 | Map yearlyReport = new HashMap<>(); 49 | report.reportTime(LocalDateTime.now().toString()); 50 | source.forEach(sur -> { 51 | for (ServiceUsageMonthlyAggregate smr: sur.getMonthlyServiceReports()) { 52 | if (monthlyReports.isEmpty()) { 53 | monthlyReports.put(smr.getServiceName(), smr); 54 | } else { 55 | ServiceUsageMonthlyAggregate existing = monthlyReports.get(smr.getServiceName()); 56 | monthlyReports.put(smr.getServiceName(), smr.combine(existing)); 57 | } 58 | } 59 | for (ServiceUsageYearlyAggregate syr: sur.getYearlyServiceReport()) { 60 | if (yearlyReport.isEmpty()) { 61 | yearlyReport.put(syr.getServiceName(), syr); 62 | } else { 63 | ServiceUsageYearlyAggregate existing = yearlyReport.get(syr.getServiceName()); 64 | yearlyReport.put(syr.getServiceName(), syr.combine(existing)); 65 | } 66 | } 67 | }); 68 | List sortedMonthlyReports = new ArrayList<>(); 69 | sortedMonthlyReports.addAll(monthlyReports.values()); 70 | sortedMonthlyReports.sort(Comparator.comparing(ServiceUsageMonthlyAggregate::getServiceName)); 71 | report.monthlyServiceReports(sortedMonthlyReports); 72 | List sortedYearlyReports = new ArrayList<>(); 73 | sortedYearlyReports.addAll(yearlyReport.values()); 74 | sortedYearlyReports.sort(Comparator.comparing(ServiceUsageYearlyAggregate::getServiceName)); 75 | report.yearlyServiceReport(sortedYearlyReports); 76 | return report.build(); 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/accounting/service/ServiceUsageYearlyAggregate.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain.accounting.service; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Comparator; 5 | import java.util.HashMap; 6 | import java.util.List; 7 | import java.util.Map; 8 | 9 | import com.fasterxml.jackson.annotation.JsonCreator; 10 | import com.fasterxml.jackson.annotation.JsonIgnore; 11 | import com.fasterxml.jackson.annotation.JsonProperty; 12 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 13 | 14 | import lombok.Builder; 15 | import lombok.Builder.Default; 16 | import lombok.Getter; 17 | 18 | @Builder 19 | @Getter 20 | @JsonPropertyOrder({"service_name", "service_guid", "year", "duration_in_hours", "maximum_instances", "average_instances", "plans"}) 21 | public class ServiceUsageYearlyAggregate { 22 | 23 | @JsonProperty("service_name") 24 | public String serviceName; 25 | 26 | @JsonProperty("service_guid") 27 | public String serviceGuid; 28 | 29 | @JsonProperty("year") 30 | public Integer year; 31 | 32 | @Default 33 | @JsonProperty("duration_in_hours") 34 | public Double durationInHours = 0.0; 35 | 36 | @Default 37 | @JsonProperty("maximum_instances") 38 | public Integer maximumInstances = 0; 39 | 40 | @Default 41 | @JsonProperty("average_instances") 42 | public Double averageInstances = 0.0; 43 | 44 | @Default 45 | @JsonProperty("plans") 46 | public List plans = new ArrayList<>(); 47 | 48 | @JsonCreator 49 | public ServiceUsageYearlyAggregate( 50 | @JsonProperty("service_name") String serviceName, 51 | @JsonProperty("service_guid") String serviceGuid, 52 | @JsonProperty("year") Integer year, 53 | @JsonProperty("duration_in_hours") Double durationInHours, 54 | @JsonProperty("maximum_instances") Integer maximumInstances, 55 | @JsonProperty("average_instances") Double averageInstances, 56 | @JsonProperty("plans") List plans) { 57 | this.serviceName = serviceName; 58 | this.serviceGuid = serviceGuid; 59 | this.year = year; 60 | this.durationInHours = durationInHours; 61 | this.maximumInstances = maximumInstances; 62 | this.averageInstances = averageInstances; 63 | this.plans = plans; 64 | } 65 | 66 | @JsonIgnore 67 | public ServiceUsageYearlyAggregate combine(ServiceUsageYearlyAggregate usage) { 68 | ServiceUsageYearlyAggregate result = null; 69 | if (usage == null) { 70 | result = this; 71 | } else if (usage.getYear().equals(year) && usage.getServiceName().equals(serviceName)) { 72 | Map yearlyPlanUsage = new HashMap<>(); 73 | usage.getPlans().forEach(pu -> { 74 | for (ServicePlanUsageYearly spu: plans) { 75 | if (yearlyPlanUsage.isEmpty()) { 76 | yearlyPlanUsage.put(spu.getYear(), spu); 77 | } else { 78 | ServicePlanUsageYearly existing = yearlyPlanUsage.get(spu.getYear()); 79 | yearlyPlanUsage.put(spu.getYear(), spu.combine(existing)); 80 | } 81 | } 82 | }); 83 | List sortedYearlyPlans = new ArrayList<>(); 84 | sortedYearlyPlans.addAll(yearlyPlanUsage.values()); 85 | sortedYearlyPlans.sort(Comparator.comparing(ServicePlanUsageYearly::getYear)); 86 | String newServiceGuid = usage.getServiceGuid(); 87 | if (!usage.getServiceGuid().contains(this.serviceGuid)) { 88 | newServiceGuid = String.join(",", this.serviceGuid, usage.getServiceGuid()); 89 | } 90 | result = 91 | ServiceUsageYearlyAggregate 92 | .builder() 93 | .serviceName(usage.getServiceName()) 94 | .serviceGuid(newServiceGuid) 95 | .averageInstances(this.averageInstances + usage.getAverageInstances()) 96 | .maximumInstances(this.maximumInstances + usage.getMaximumInstances()) 97 | .durationInHours(this.durationInHours + usage.getDurationInHours()) 98 | .plans(sortedYearlyPlans) 99 | .build(); 100 | } else { 101 | result = usage; 102 | } 103 | return result; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/accounting/task/TaskUsageMonthly.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain.accounting.task; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonIgnore; 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 7 | 8 | import lombok.Builder; 9 | import lombok.Builder.Default; 10 | import lombok.Getter; 11 | 12 | @Builder 13 | @Getter 14 | @JsonPropertyOrder({"month", "year", "total_task_runs", "maximum_concurrent_tasks", "task_hours"}) 15 | public class TaskUsageMonthly { 16 | 17 | @JsonProperty("month") 18 | private Integer month; 19 | 20 | @JsonProperty("year") 21 | private Integer year; 22 | 23 | @Default 24 | @JsonProperty("total_task_runs") 25 | private Integer totalTaskRuns = 0; 26 | 27 | @Default 28 | @JsonProperty("maximum_concurrent_tasks") 29 | private Integer maximumConcurrentTasks = 0; 30 | 31 | @Default 32 | @JsonProperty("task_hours") 33 | private Double taskHours = 0.0; 34 | 35 | @JsonCreator 36 | public TaskUsageMonthly( 37 | @JsonProperty("month") Integer month, 38 | @JsonProperty("year") Integer year, 39 | @JsonProperty("total_task_runs") Integer totalTaskRuns, 40 | @JsonProperty("maximum_concurrent_tasks") Integer maximumConcurrentTasks, 41 | @JsonProperty("task_hours") Double taskHours) { 42 | this.month = month; 43 | this.year = year; 44 | this.totalTaskRuns = totalTaskRuns; 45 | this.maximumConcurrentTasks = maximumConcurrentTasks; 46 | this.taskHours = taskHours; 47 | } 48 | 49 | @JsonIgnore 50 | public TaskUsageMonthly combine(TaskUsageMonthly usage) { 51 | TaskUsageMonthly result = null; 52 | if (usage == null) { 53 | result = this; 54 | } else if (usage.getMonth().equals(month) && usage.getYear().equals(year)) { 55 | result = 56 | TaskUsageMonthly 57 | .builder() 58 | .year(usage.getYear()) 59 | .month(usage.getMonth()) 60 | .totalTaskRuns(this.totalTaskRuns + usage.getTotalTaskRuns()) 61 | .maximumConcurrentTasks(this.maximumConcurrentTasks + usage.getMaximumConcurrentTasks()) 62 | .taskHours(this.taskHours + usage.getTaskHours()) 63 | .build(); 64 | } else { 65 | result = usage; 66 | } 67 | return result; 68 | } 69 | 70 | @JsonIgnore 71 | public String getYearAndMonth() { 72 | return String.join("-", String.valueOf(year), String.format("%02d", month)); 73 | } 74 | } -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/accounting/task/TaskUsageReport.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain.accounting.task; 2 | 3 | import java.time.LocalDateTime; 4 | import java.util.ArrayList; 5 | import java.util.Comparator; 6 | import java.util.HashMap; 7 | import java.util.List; 8 | import java.util.Map; 9 | 10 | import com.fasterxml.jackson.annotation.JsonCreator; 11 | import com.fasterxml.jackson.annotation.JsonProperty; 12 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 13 | 14 | import lombok.Builder; 15 | import lombok.Builder.Default; 16 | import lombok.Getter; 17 | 18 | @Builder 19 | @Getter 20 | @JsonPropertyOrder({"report_time", "monthly_reports", "yearly_reports"}) 21 | public class TaskUsageReport { 22 | 23 | @JsonProperty("report_time") 24 | private String reportTime; 25 | 26 | @Default 27 | @JsonProperty("monthly_reports") 28 | private List monthlyReports = new ArrayList<>(); 29 | 30 | @Default 31 | @JsonProperty("yearly_reports") 32 | private List yearlyReports = new ArrayList<>(); 33 | 34 | @JsonCreator 35 | public TaskUsageReport( 36 | @JsonProperty("report_time") String reportTime, 37 | @JsonProperty("monthly_reports") List monthlyReports, 38 | @JsonProperty("yearly_reports") List yearlyReports) { 39 | this.reportTime = reportTime; 40 | this.monthlyReports = monthlyReports; 41 | this.yearlyReports = yearlyReports; 42 | } 43 | 44 | 45 | public static TaskUsageReport aggregate(List source) { 46 | TaskUsageReportBuilder report = TaskUsageReport.builder(); 47 | Map monthlyReports = new HashMap<>(); 48 | Map yearlyReports = new HashMap<>(); 49 | report.reportTime(LocalDateTime.now().toString()); 50 | source.forEach(aur -> { 51 | for (TaskUsageMonthly smr: aur.getMonthlyReports()) { 52 | if (monthlyReports.isEmpty()) { 53 | monthlyReports.put(smr.getYearAndMonth(), smr); 54 | } else { 55 | TaskUsageMonthly existing = monthlyReports.get(smr.getYearAndMonth()); 56 | monthlyReports.put(smr.getYearAndMonth(), smr.combine(existing)); 57 | } 58 | } 59 | for (TaskUsageYearly syr: aur.getYearlyReports()) { 60 | if (yearlyReports.isEmpty()) { 61 | yearlyReports.put(syr.getYear(), syr); 62 | } else { 63 | TaskUsageYearly existing = yearlyReports.get(syr.getYear()); 64 | yearlyReports.put(syr.getYear(), syr.combine(existing)); 65 | } 66 | } 67 | }); 68 | List sortedMonthlyReports = new ArrayList<>(); 69 | sortedMonthlyReports.addAll(monthlyReports.values()); 70 | sortedMonthlyReports.sort(Comparator.comparing(TaskUsageMonthly::getYearAndMonth)); 71 | report.monthlyReports(sortedMonthlyReports); 72 | List sortedYearlyReports = new ArrayList<>(); 73 | sortedYearlyReports.addAll(yearlyReports.values()); 74 | sortedYearlyReports.sort(Comparator.comparing(TaskUsageYearly::getYear)); 75 | report.yearlyReports(sortedYearlyReports); 76 | return report.build(); 77 | } 78 | } 79 | 80 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/accounting/task/TaskUsageYearly.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain.accounting.task; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonIgnore; 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 7 | 8 | import lombok.Builder; 9 | import lombok.Builder.Default; 10 | import lombok.Getter; 11 | 12 | @Builder 13 | @Getter 14 | @JsonPropertyOrder({"year", "total_task_runs", "maximum_concurrent_tasks", "task_hours"}) 15 | public class TaskUsageYearly { 16 | 17 | @JsonProperty("year") 18 | private Integer year; 19 | 20 | @Default 21 | @JsonProperty("total_task_runs") 22 | private Integer totalTaskRuns = 0; 23 | 24 | @Default 25 | @JsonProperty("maximum_concurrent_tasks") 26 | private Integer maximumConcurrentTasks = 0; 27 | 28 | @Default 29 | @JsonProperty("task_hours") 30 | private Double taskHours = 0.0; 31 | 32 | @JsonCreator 33 | public TaskUsageYearly( 34 | @JsonProperty("year") Integer year, 35 | @JsonProperty("total_task_runs") Integer totalTaskRuns, 36 | @JsonProperty("maximum_concurrent_tasks") Integer maximumConcurrentTasks, 37 | @JsonProperty("task_hours") Double taskHours) { 38 | this.year = year; 39 | this.totalTaskRuns = totalTaskRuns; 40 | this.maximumConcurrentTasks = maximumConcurrentTasks; 41 | this.taskHours = taskHours; 42 | } 43 | 44 | @JsonIgnore 45 | public TaskUsageYearly combine(TaskUsageYearly usage) { 46 | TaskUsageYearly result = null; 47 | if (usage == null) { 48 | result = this; 49 | } else if (usage.getYear().equals(year)) { 50 | result = 51 | TaskUsageYearly 52 | .builder() 53 | .year(usage.getYear()) 54 | .totalTaskRuns(this.totalTaskRuns + usage.getTotalTaskRuns()) 55 | .maximumConcurrentTasks(this.maximumConcurrentTasks + usage.getMaximumConcurrentTasks()) 56 | .taskHours(this.taskHours + usage.getTaskHours()) 57 | .build(); 58 | } else { 59 | result = usage; 60 | } 61 | return result; 62 | } 63 | } -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/report/AppDetailCsvReport.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.report; 2 | 3 | import org.cftoolsuite.cfapp.domain.AppDetail; 4 | import org.cftoolsuite.cfapp.task.AppDetailRetrievedEvent; 5 | 6 | public class AppDetailCsvReport { 7 | 8 | public String generateDetail(AppDetailRetrievedEvent event) { 9 | StringBuffer details = new StringBuffer(); 10 | details.append("\n"); 11 | details.append(AppDetail.headers()); 12 | details.append("\n"); 13 | event.getDetail() 14 | .forEach(a -> { 15 | details.append(a.toCsv()); 16 | details.append("\n"); 17 | }); 18 | return details.toString(); 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/report/AppRelationshipCsvReport.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.report; 2 | 3 | import org.cftoolsuite.cfapp.domain.AppRelationship; 4 | import org.cftoolsuite.cfapp.task.AppRelationshipRetrievedEvent; 5 | 6 | public class AppRelationshipCsvReport { 7 | 8 | public String generateDetail(AppRelationshipRetrievedEvent event) { 9 | StringBuffer details = new StringBuffer(); 10 | details.append("\n"); 11 | details.append(AppRelationship.headers()); 12 | details.append("\n"); 13 | event.getRelations() 14 | .forEach(a -> { 15 | details.append(a.toCsv()); 16 | details.append("\n"); 17 | }); 18 | return details.toString(); 19 | } 20 | } -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/report/ServiceInstanceDetailCsvReport.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.report; 2 | 3 | import org.cftoolsuite.cfapp.domain.ServiceInstanceDetail; 4 | import org.cftoolsuite.cfapp.task.ServiceInstanceDetailRetrievedEvent; 5 | 6 | public class ServiceInstanceDetailCsvReport { 7 | 8 | public String generateDetail(ServiceInstanceDetailRetrievedEvent event) { 9 | StringBuffer detail = new StringBuffer(); 10 | detail.append("\n"); 11 | detail.append(ServiceInstanceDetail.headers()); 12 | detail.append("\n"); 13 | event.getDetail() 14 | .forEach(a -> { 15 | detail.append(a.toCsv()); 16 | detail.append("\n"); 17 | }); 18 | return detail.toString(); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/task/AppDetailRetrievedEvent.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.task; 2 | 3 | import java.util.List; 4 | 5 | import org.springframework.context.ApplicationEvent; 6 | 7 | import org.cftoolsuite.cfapp.domain.AppDetail; 8 | 9 | public class AppDetailRetrievedEvent extends ApplicationEvent { 10 | 11 | private static final long serialVersionUID = 1L; 12 | 13 | private List detail; 14 | 15 | public AppDetailRetrievedEvent(Object source) { 16 | super(source); 17 | } 18 | 19 | public AppDetailRetrievedEvent detail(List detail) { 20 | this.detail = detail; 21 | return this; 22 | } 23 | 24 | public List getDetail() { 25 | return detail; 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/task/AppRelationshipRetrievedEvent.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.task; 2 | 3 | import java.util.List; 4 | 5 | import org.springframework.context.ApplicationEvent; 6 | 7 | import org.cftoolsuite.cfapp.domain.AppRelationship; 8 | 9 | public class AppRelationshipRetrievedEvent extends ApplicationEvent { 10 | 11 | private static final long serialVersionUID = 1L; 12 | 13 | private List relations; 14 | 15 | public AppRelationshipRetrievedEvent(Object source) { 16 | super(source); 17 | } 18 | 19 | public AppRelationshipRetrievedEvent relations(List relations) { 20 | this.relations = relations; 21 | return this; 22 | } 23 | 24 | public List getRelations() { 25 | return relations; 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/task/ServiceInstanceDetailRetrievedEvent.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.task; 2 | 3 | import java.util.List; 4 | 5 | import org.springframework.context.ApplicationEvent; 6 | 7 | import org.cftoolsuite.cfapp.domain.ServiceInstanceDetail; 8 | 9 | public class ServiceInstanceDetailRetrievedEvent extends ApplicationEvent { 10 | 11 | private static final long serialVersionUID = 1L; 12 | 13 | private List detail; 14 | 15 | public ServiceInstanceDetailRetrievedEvent(Object source) { 16 | super(source); 17 | } 18 | 19 | public ServiceInstanceDetailRetrievedEvent detail(List detail) { 20 | this.detail = detail; 21 | return this; 22 | } 23 | 24 | public List getDetail() { 25 | return detail; 26 | } 27 | 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | cf: 2 | butlers: [] 3 | sslValidationSkipped: false 4 | 5 | spring: 6 | application: 7 | name: cf-hoover 8 | codec: 9 | max-in-memory-size: 512000000 10 | cloud: 11 | config: 12 | enabled: false 13 | discovery: 14 | enabled: false 15 | threads: 16 | virtual: 17 | enabled: true 18 | 19 | eureka: 20 | client: 21 | serviceUrl: 22 | defaultZone: ${vcap.services.hooverRegistry.credentials.uri:http://localhost:8761}/eureka/ 23 | 24 | management: 25 | info: 26 | build: 27 | enabled: true 28 | dependencies: 29 | enabled: true 30 | env: 31 | enabled: true 32 | git: 33 | mode: full 34 | java: 35 | enabled: true 36 | os: 37 | enabled: true 38 | sbom: 39 | enabled: true 40 | endpoints: 41 | web: 42 | exposure: 43 | include: health,info,httptrace,loggers,metrics,prometheus,sbom 44 | endpoint: 45 | health: 46 | show-details: always 47 | 48 | --- 49 | spring: 50 | config: 51 | activate: 52 | on-profile: cloud 53 | 54 | cloud: 55 | config: 56 | enabled: true 57 | discovery: 58 | enabled: true 59 | threads: 60 | virtual: 61 | enabled: true 62 | 63 | management: 64 | cloudfoundry: 65 | enabled: true 66 | skip-ssl-validation: true -------------------------------------------------------------------------------- /src/main/resources/log4j2-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 | 12 | 13 | %d %p %C{1.} [%t] %m%n 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/main/resources/log4j2.component.properties: -------------------------------------------------------------------------------- 1 | log4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector -------------------------------------------------------------------------------- /src/main/resources/logback-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 10 | 11 | 12 | 13 | 14 | ${LOG_ROOT}/${LOG_FILE_NAME}.log 15 | 16 | ${LOG_ROOT}/${LOG_FILE_NAME}-%d{yyyy-MM-dd}.%i.log.gz 17 | 18 | 10MB 19 | 20 | 30 21 | 22 | 100GB 23 | 24 | 25 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/test/java/org/cftoolsuite/cfapp/CfHooverApplicationTests.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | import org.springframework.test.context.TestPropertySource; 6 | 7 | @SpringBootTest 8 | @TestPropertySource(properties = "cf.butlers.example=https://cf-butler.example.com") 9 | public class CfHooverApplicationTests { 10 | 11 | @Test 12 | public void contextLoads() { 13 | } 14 | 15 | } 16 | --------------------------------------------------------------------------------