├── .sdkmanrc ├── src ├── test │ ├── resources │ │ ├── test.properties │ │ ├── email-templates │ │ │ ├── simple-email.json │ │ │ └── email-with-attachments.json │ │ └── access-filter.json │ └── java │ │ └── org │ │ └── cftoolsuite │ │ └── cfapp │ │ ├── ButlerTest.java │ │ ├── domain │ │ └── OrganizationTest.java │ │ ├── service │ │ ├── ReportRequestSpec.java │ │ ├── ServiceInstanceReporterTest.java │ │ └── ApplicationReporterTest.java │ │ ├── repository │ │ ├── R2dbcTimeKeeperRepositoryTest.java │ │ ├── ListRemoteRepositoryExample.java │ │ ├── R2dbcOrganizationRepositoryTest.java │ │ └── R2dbcSpaceRepositoryTest.java │ │ └── util │ │ └── JsonToCsvConverterTest.java └── main │ ├── resources │ ├── log4j2.component.properties │ ├── META-INF │ │ └── spring.factories │ ├── email-template.html │ ├── logback-spring.xml │ └── log4j2-spring.xml │ └── java │ └── org │ └── cftoolsuite │ └── cfapp │ ├── domain │ ├── Policy.java │ ├── HasIdentifier.java │ ├── HasOrganizationWhiteList.java │ ├── HasCronExpression.java │ ├── EmailValidator.java │ ├── WorkloadsFilter.java │ ├── Stack.java │ ├── Event.java │ ├── ObjectUtils.java │ ├── product │ │ ├── UserGroups.java │ │ ├── EulaAcceptance.java │ │ ├── EulaLinks.java │ │ ├── Self.java │ │ ├── FileGroups.java │ │ ├── ProductFiles.java │ │ ├── ArtifactReferences.java │ │ ├── Releases.java │ │ ├── ProductMetrics.java │ │ ├── Products.java │ │ ├── ProductType.java │ │ ├── StemcellDetail.java │ │ ├── StemcellAssignments.java │ │ ├── StemcellAssociations.java │ │ ├── Staleness.java │ │ ├── ProductLinks.java │ │ ├── Eula.java │ │ ├── OmInfo.java │ │ └── ReleaseLinks.java │ ├── Href.java │ ├── TimeKeeper.java │ ├── SnapshotSummary.java │ ├── Buildpack.java │ ├── Metadata.java │ ├── event │ │ ├── Resource.java │ │ └── ResourceMetadata.java │ ├── Resources.java │ ├── EmailAttachment.java │ ├── ApplicationState.java │ ├── SpaceUsersReadConverter.java │ ├── Organization.java │ ├── ServiceInstanceCounts.java │ ├── CustomConverters.java │ ├── SnapshotDetail.java │ ├── accounting │ │ ├── service │ │ │ ├── ServicePlanUsageMonthly.java │ │ │ ├── ServiceUsageReport.java │ │ │ ├── ServiceUsageMonthlyAggregate.java │ │ │ └── ServiceUsageMonthly.java │ │ ├── task │ │ │ ├── TaskUsageReport.java │ │ │ ├── TaskUsageYearly.java │ │ │ └── TaskUsageMonthly.java │ │ └── application │ │ │ ├── AppUsageReport.java │ │ │ ├── AppUsageYearly.java │ │ │ └── AppUsageMonthly.java │ ├── EndpointRequest.java │ ├── Resource.java │ ├── Query.java │ ├── Pagination.java │ ├── OwnerNotificationTemplate.java │ ├── SpaceUsersWriteConverter.java │ ├── AppRelationshipRequest.java │ ├── UserCounts.java │ ├── Defaults.java │ ├── Demographics.java │ ├── UserSpaces.java │ ├── HistoricalRecord.java │ ├── ServiceInstanceOperation.java │ ├── AppRelationship.java │ ├── Space.java │ └── ResourceType.java │ ├── task │ ├── PolicyExecutorTask.java │ ├── AppDetailReadyToBeCollectedDecider.java │ ├── SpacesRetrievedListener.java │ └── ProductsAndReleasesRetrievedListener.java │ ├── util │ ├── JavaArtifactReader.java │ ├── JarManifestUtil.java │ ├── DbmsOnlyCondition.java │ ├── DropletProcessingCondition.java │ ├── JarSetFilterReaderCondition.java │ ├── CsvUtil.java │ ├── RetryableTokenProvider.java │ └── PolicyFilter.java │ ├── config │ ├── PivnetSettings.java │ ├── DbmsSettings.java │ ├── OpsmanSettings.java │ ├── JacksonDeSerConfig.java │ ├── H2ConsoleConfig.java │ └── GitSettings.java │ ├── event │ ├── DatabaseCreatedEvent.java │ ├── StacksRetrievedEvent.java │ ├── BuildpacksRetrievedEvent.java │ ├── AppDetailReadyToBeRetrievedEvent.java │ ├── PoliciesLoadedEvent.java │ ├── SpacesRetrievedEvent.java │ ├── TkRetrievedEvent.java │ ├── AppDetailRetrievedEvent.java │ ├── UserAccountsRetrievedEvent.java │ ├── HistoricalRecordRetrievedEvent.java │ ├── AppRelationshipRetrievedEvent.java │ ├── OrganizationsRetrievedEvent.java │ ├── ServiceInstanceDetailRetrievedEvent.java │ └── ProductsAndReleasesRetrievedEvent.java │ ├── service │ ├── SpaceService.java │ ├── QueryService.java │ ├── OrganizationService.java │ ├── HistoricalRecordService.java │ ├── ServiceInstanceMetricsService.java │ ├── AppRelationshipService.java │ ├── UsageCache.java │ ├── JavaAppDetailService.java │ ├── AppDetailService.java │ ├── ServiceInstanceDetailService.java │ ├── AppMetricsService.java │ ├── SpaceUsersService.java │ ├── TkServiceUtil.java │ ├── AccountMatcher.java │ ├── UserSpacesService.java │ ├── ReportRequest.java │ ├── R2dbcSpaceService.java │ ├── R2dbcQueryService.java │ ├── TimeKeeperService.java │ ├── R2dbcOrganizationService.java │ ├── R2dbcServiceInstanceMetricsService.java │ ├── BuildpacksCache.java │ ├── R2dbcHistoricalRecordService.java │ ├── StacksCache.java │ └── R2dbcAppRelationshipService.java │ ├── AppInit.java │ ├── repository │ ├── R2dbcQueryRepository.java │ ├── R2dbcTimeKeeperRepository.java │ ├── R2dbcSpaceRepository.java │ └── R2dbcOrganizationRepository.java │ ├── controller │ ├── OnDemandCollectorTriggerController.java │ ├── ProductMetricsController.java │ ├── JavaAppDetailController.java │ ├── DemographicsController.java │ ├── UserSpacesController.java │ └── OnDemandPolicyTriggerController.java │ ├── deser │ └── RelaxedLocalDateDeserializer.java │ ├── report │ ├── HistoricalRecordCsvReport.java │ ├── AppDetailCsvReport.java │ ├── UserAccountsCsvReport.java │ ├── AppRelationshipCsvReport.java │ └── ServiceInstanceDetailCsvReport.java │ └── notifier │ ├── AppDetailConsoleNotifier.java │ ├── AppRelationshipConsoleNotifier.java │ ├── ProductsAndReleasesConsoleNotifier.java │ └── ServiceInstanceDetailConsoleNotifier.java ├── docs ├── cf-butler-2024.png ├── CLONING.md ├── TOOLS.md ├── SONARQUBE.md ├── CREDITS.md ├── PREREQUISITES.md ├── INTEGRATIONS.md └── BUILD.md ├── samples ├── application-mysql.yml ├── application-postgres.yml ├── secrets.pcfone.json ├── application-pws.yml ├── application-pcfone.yml ├── secrets.pws.json ├── secrets.pws.with-postgres.json └── secrets.pws.with-mysql.json ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── scripts ├── destroy.ps1 ├── destroy.sh ├── destroy.mysql.sh ├── destroy.postgres.sh ├── destroy.mysql.ps1 ├── destroy.postgres.ps1 ├── deploy.sh ├── deploy.ps1 ├── process-java-app-dependencies-tarball.sh ├── deploy.cf4k8s.sh ├── deploy.mysql.sh ├── deploy.postgres.sh └── deploy.alt.sh ├── manifest.yml ├── .gitignore ├── .mvn └── wrapper │ └── maven-wrapper.properties └── README.md /.sdkmanrc: -------------------------------------------------------------------------------- 1 | java=21.0.2-graalce -------------------------------------------------------------------------------- /src/test/resources/test.properties: -------------------------------------------------------------------------------- 1 | om.username=foo 2 | om.password=bar -------------------------------------------------------------------------------- /docs/cf-butler-2024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cf-toolsuite/cf-butler/HEAD/docs/cf-butler-2024.png -------------------------------------------------------------------------------- /src/main/resources/log4j2.component.properties: -------------------------------------------------------------------------------- 1 | log4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector -------------------------------------------------------------------------------- /src/main/resources/META-INF/spring.factories: -------------------------------------------------------------------------------- 1 | io.pivotal.cfenv.spring.boot.CfEnvProcessor=org.cftoolsuite.cfapp.config.ButlerCfEnvProcessor -------------------------------------------------------------------------------- /docs/CLONING.md: -------------------------------------------------------------------------------- 1 | # VMware Tanzu Application Service > Butler 2 | 3 | ## Clone 4 | 5 | ``` 6 | git clone https://github.com/cf-toolsuite/cf-butler.git 7 | ``` 8 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/Policy.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain; 2 | 3 | public interface Policy extends HasIdentifier, HasCronExpression {} 4 | -------------------------------------------------------------------------------- /samples/application-mysql.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | r2dbc: 3 | url: r2dbc:mysql://localhost:3306/butler 4 | name: butler 5 | username: butler 6 | password: p@ssw0rd 7 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/HasIdentifier.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain; 2 | 3 | public interface HasIdentifier { 4 | String getId(); 5 | } 6 | -------------------------------------------------------------------------------- /samples/application-postgres.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | r2dbc: 3 | url: r2dbc:postgresql://localhost:5432/butler 4 | name: butler 5 | username: butler 6 | password: p@ssw0rd 7 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/task/PolicyExecutorTask.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.task; 2 | 3 | public interface PolicyExecutorTask { 4 | void execute(String policyId); 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/HasOrganizationWhiteList.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain; 2 | 3 | import java.util.Set; 4 | 5 | public interface HasOrganizationWhiteList { 6 | Set getOrganizationWhiteList(); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/util/JavaArtifactReader.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.util; 2 | 3 | import java.util.Set; 4 | 5 | public interface JavaArtifactReader { 6 | 7 | Set read(String input); 8 | String mode(); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/HasCronExpression.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain; 2 | 3 | public interface HasCronExpression { 4 | String getCronExpression(); 5 | 6 | default String defaultCronExpression() { 7 | return "0 0 2 * * MON"; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/EmailValidator.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain; 2 | 3 | import com.sanctionco.jmail.JMail; 4 | 5 | public class EmailValidator { 6 | 7 | public static boolean isValid(String email) { 8 | return JMail.isValid(email); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /scripts/destroy.ps1: -------------------------------------------------------------------------------- 1 | #!\usr\bin\env pwsh 2 | 3 | $AppName=cf-butler 4 | 5 | $AppId = (cf app $AppName --guid) | Out-String 6 | 7 | if ($AppId) { 8 | cf stop $AppName 9 | cf unbind-service $AppName $AppName-secrets 10 | cf delete-service $AppName-secrets -f 11 | cf delete $AppName -r -f 12 | } else { 13 | Write-Host "$AppName does not exist" 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/config/PivnetSettings.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.config; 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties; 4 | 5 | import lombok.Data; 6 | 7 | @Data 8 | @ConfigurationProperties(prefix = "pivnet") 9 | public class PivnetSettings { 10 | 11 | private boolean enabled; 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/WorkloadsFilter.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain; 2 | 3 | import java.util.Set; 4 | 5 | import lombok.Builder; 6 | import lombok.Getter; 7 | 8 | @Builder 9 | @Getter 10 | public class WorkloadsFilter { 11 | 12 | private Set stacks; 13 | private Set serviceOfferings; 14 | 15 | } 16 | -------------------------------------------------------------------------------- /scripts/destroy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -x 4 | 5 | export APP_NAME=cf-butler 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-secrets 12 | cf delete-service $APP_NAME-secrets -f 13 | cf delete $APP_NAME -r -f 14 | else 15 | echo "$APP_NAME does not exist" 16 | fi 17 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/Stack.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain; 2 | 3 | import lombok.Builder; 4 | import lombok.Getter; 5 | import lombok.ToString; 6 | 7 | @Builder 8 | @Getter 9 | @ToString 10 | public class Stack { 11 | 12 | private final String id; 13 | private final String name; 14 | private final String description; 15 | } 16 | -------------------------------------------------------------------------------- /samples/secrets.pcfone.json: -------------------------------------------------------------------------------- 1 | { 2 | "PIVNET_API-TOKEN": "xxxxxx", 3 | "CF_TOKEN-PROVIDER": "sso", 4 | "CF_API-HOST": "api.run.pcfone.io", 5 | "CF_REFRESH-TOKEN": "xxxxxx", 6 | "CF_ORGANIZATION-BLACK-LIST": [ "system" ], 7 | "CF_ACCOUNT-REGEX": "^[_A-Za-z0-9-\\+]+(\\.[_A-Za-z0-9-]+)*@[A-Za-z0-9-]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$", 8 | "CRON_COLLECTION": "0 0 0 * * *" 9 | } -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/Event.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain; 2 | 3 | import java.time.LocalDateTime; 4 | 5 | import lombok.Builder; 6 | import lombok.Getter; 7 | 8 | @Builder 9 | @Getter 10 | public class Event { 11 | 12 | private String type; 13 | private String actee; 14 | private String actor; 15 | private LocalDateTime time; 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/event/DatabaseCreatedEvent.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.event; 2 | 3 | import org.springframework.context.ApplicationEvent; 4 | 5 | public class DatabaseCreatedEvent extends ApplicationEvent { 6 | 7 | private static final long serialVersionUID = 1L; 8 | 9 | public DatabaseCreatedEvent(Object source) { 10 | super(source); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/event/StacksRetrievedEvent.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.event; 2 | 3 | import org.springframework.context.ApplicationEvent; 4 | 5 | public class StacksRetrievedEvent extends ApplicationEvent { 6 | 7 | private static final long serialVersionUID = 1L; 8 | 9 | public StacksRetrievedEvent(Object source) { 10 | super(source); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/service/SpaceService.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.service; 2 | 3 | import org.cftoolsuite.cfapp.domain.Space; 4 | 5 | import reactor.core.publisher.Flux; 6 | import reactor.core.publisher.Mono; 7 | 8 | public interface SpaceService { 9 | 10 | Mono deleteAll(); 11 | 12 | Flux findAll(); 13 | 14 | Mono save(Space entity); 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/event/BuildpacksRetrievedEvent.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.event; 2 | 3 | import org.springframework.context.ApplicationEvent; 4 | 5 | public class BuildpacksRetrievedEvent extends ApplicationEvent { 6 | 7 | private static final long serialVersionUID = 1L; 8 | 9 | public BuildpacksRetrievedEvent(Object source) { 10 | super(source); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/service/QueryService.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.service; 2 | 3 | import org.cftoolsuite.cfapp.domain.Query; 4 | 5 | import io.r2dbc.spi.Row; 6 | import io.r2dbc.spi.RowMetadata; 7 | import reactor.core.publisher.Flux; 8 | import reactor.util.function.Tuple2; 9 | 10 | public interface QueryService { 11 | 12 | Flux> executeQuery(Query query); 13 | } 14 | -------------------------------------------------------------------------------- /src/test/resources/email-templates/simple-email.json: -------------------------------------------------------------------------------- 1 | { 2 | "from": "sender@example.com", 3 | "recipients": ["recipient1@example.com", "recipient2@example.com"], 4 | "carbonCopyRecipients": ["cc1@example.com"], 5 | "blindCarbonCopyRecipients": ["bcc1@example.com"], 6 | "subject": "Simple Email Test", 7 | "body": "This is a simple test email.", 8 | "domain": "example.com", 9 | "attachments": [] 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/event/AppDetailReadyToBeRetrievedEvent.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.event; 2 | 3 | import org.springframework.context.ApplicationEvent; 4 | 5 | public class AppDetailReadyToBeRetrievedEvent extends ApplicationEvent { 6 | 7 | private static final long serialVersionUID = 1L; 8 | 9 | public AppDetailReadyToBeRetrievedEvent(Object source) { 10 | super(source); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /scripts/destroy.mysql.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -x 4 | 5 | export APP_NAME=cf-butler 6 | 7 | if cf app $APP_NAME --guid; then 8 | cf stop $APP_NAME 9 | cf unbind-service $APP_NAME $APP_NAME-secrets 10 | cf delete-service $APP_NAME-secrets -f 11 | cf unbind-service $APP_NAME $APP_NAME-backend 12 | cf delete-service $APP_NAME-backend -f 13 | cf delete $APP_NAME -r -f 14 | else 15 | echo "$APP_NAME does not exist" 16 | fi 17 | -------------------------------------------------------------------------------- /scripts/destroy.postgres.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -x 4 | 5 | export APP_NAME=cf-butler 6 | 7 | if cf app $APP_NAME --guid; then 8 | cf stop $APP_NAME 9 | cf unbind-service $APP_NAME $APP_NAME-secrets 10 | cf delete-service $APP_NAME-secrets -f 11 | cf unbind-service $APP_NAME $APP_NAME-backend 12 | cf delete-service $APP_NAME-backend -f 13 | cf delete $APP_NAME -r -f 14 | else 15 | echo "$APP_NAME does not exist" 16 | fi 17 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/ObjectUtils.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain; 2 | 3 | import java.util.Collection; 4 | 5 | public class ObjectUtils { 6 | 7 | public static boolean isEmpty(Object[] array) { 8 | return array == null || array.length == 0; 9 | } 10 | 11 | public static boolean isEmpty(Collection collection) { 12 | return collection == null || collection.isEmpty(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /scripts/destroy.mysql.ps1: -------------------------------------------------------------------------------- 1 | #!\usr\bin\env pwsh 2 | 3 | $AppName=cf-butler 4 | 5 | $AppId = (cf app $AppName --guid) | Out-String 6 | 7 | if ($AppId) { 8 | cf stop $AppName 9 | cf unbind-service $AppName $AppName-secrets 10 | cf delete-service $AppName-secrets -f 11 | cf unbind-service $AppName $AppName-backend 12 | cf delete-service $AppName-backend -f 13 | cf delete $AppName -r -f 14 | else { 15 | Write-Host "$AppName does not exist" 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/service/OrganizationService.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.service; 2 | 3 | import org.cftoolsuite.cfapp.domain.Organization; 4 | 5 | import reactor.core.publisher.Flux; 6 | import reactor.core.publisher.Mono; 7 | 8 | public interface OrganizationService { 9 | 10 | Mono deleteAll(); 11 | 12 | Flux findAll(); 13 | 14 | Mono save(Organization entity); 15 | 16 | } 17 | -------------------------------------------------------------------------------- /scripts/destroy.postgres.ps1: -------------------------------------------------------------------------------- 1 | #!\usr\bin\env pwsh 2 | 3 | $AppName=cf-butler 4 | 5 | $AppId = (cf app $AppName --guid) | Out-String 6 | 7 | if ($AppId) { 8 | cf stop $AppName 9 | cf unbind-service $AppName $AppName-secrets 10 | cf delete-service $AppName-secrets -f 11 | cf unbind-service $AppName $AppName-backend 12 | cf delete-service $AppName-backend -f 13 | cf delete $AppName -r -f 14 | else { 15 | Write-Host "$AppName does not exist" 16 | } 17 | -------------------------------------------------------------------------------- /docs/TOOLS.md: -------------------------------------------------------------------------------- 1 | # VMware Tanzu Application Service > Butler 2 | 3 | ## Tools 4 | 5 | * [cf](https://docs.cloudfoundry.org/cf-cli/install-go-cli.html) CLI 8.6.1 or better 6 | * [git](https://git-scm.com/downloads) 2.40.0 or better 7 | * [gh](https://github.com/cli/cli) 2.42.0 or better 8 | * [http](https://httpie.io/) 9 | * [JDK](http://openjdk.java.net/install/) 21 or better 10 | * [sdkman](https://sdkman.io) 11 | * [uaac](https://github.com/cloudfoundry/cf-uaac) 4.14.0 or better 12 | -------------------------------------------------------------------------------- /manifest.yml: -------------------------------------------------------------------------------- 1 | --- 2 | applications: 3 | - name: cf-butler 4 | memory: 3G 5 | stack: cflinuxfs4 6 | path: target/cf-butler-1.0-SNAPSHOT.jar 7 | instances: 1 8 | env: 9 | JAVA_OPTS: -Djava.security.egd=file:///dev/urandom -XX:+UseG1GC -XX:SoftRefLRUPolicyMSPerMB=1 -XX:+UseStringDeduplication -XX:MaxDirectMemorySize=1G 10 | SPRING_PROFILES_ACTIVE: on-demand,cloud 11 | JBP_CONFIG_OPEN_JDK_JRE: '{ jre: { version: 25.+ } }' 12 | JBP_CONFIG_SPRING_AUTO_RECONFIGURATION: '{ enabled: false }' 13 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/service/HistoricalRecordService.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.service; 2 | 3 | import java.time.LocalDate; 4 | 5 | import org.cftoolsuite.cfapp.domain.HistoricalRecord; 6 | 7 | import reactor.core.publisher.Flux; 8 | import reactor.core.publisher.Mono; 9 | 10 | public interface HistoricalRecordService { 11 | 12 | Flux findAll(); 13 | Flux findByDateRange(LocalDate start, LocalDate end); 14 | Mono save(HistoricalRecord entity); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/resources/email-template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | cf-butler notice 6 | 7 | 8 | 9 |
10 | {{header}} 11 |
12 |
13 | {{body}} 14 |
15 |
16 | {{footer}} 17 |
18 | 19 | -------------------------------------------------------------------------------- /docs/SONARQUBE.md: -------------------------------------------------------------------------------- 1 | # VMware Tanzu Application Service > Butler 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.token=cf-butler -Dsonar.login=admin -Dsonar.password=admin 15 | ``` 16 | 17 | Then visit `http://localhost:9000` in your favorite browser to inspect results of scan. -------------------------------------------------------------------------------- /samples/application-pws.yml: -------------------------------------------------------------------------------- 1 | cf: 2 | apiHost: api.run.pivotal.io 3 | username: you@mail.me 4 | password: xxxxxx 5 | organizationBlackList: 6 | - system 7 | 8 | logging: 9 | level: 10 | org.springframework: INFO 11 | org.cloudfoundry.reactor: DEBUG 12 | 13 | # Set schedule for this task to adhere to 14 | # @see https://crontab.guru for help, first parameter is seconds 15 | cron: 16 | collection: "0 0 0 * * *" 17 | 18 | management: 19 | endpoints: 20 | web: 21 | exposure: 22 | include: info,health,metrics,scheduledtasks,loggers,prometheus 23 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/product/UserGroups.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain.product; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 5 | 6 | import lombok.Builder; 7 | import lombok.Getter; 8 | 9 | @Builder 10 | @Getter 11 | @JsonPropertyOrder({ 12 | "href" 13 | }) 14 | public class UserGroups { 15 | 16 | @JsonProperty("href") 17 | private String href; 18 | 19 | public UserGroups(@JsonProperty("href") String href) { 20 | this.href = href; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /samples/application-pcfone.yml: -------------------------------------------------------------------------------- 1 | cf: 2 | apiHost: api.run.pcfone.io 3 | refreshToken: xxxxxx 4 | organizationBlackList: 5 | - system 6 | tokenProvider: sso 7 | 8 | logging: 9 | level: 10 | org.springframework: INFO 11 | org.cloudfoundry.reactor: DEBUG 12 | 13 | # Set schedule for this task to adhere to 14 | # @see https://crontab.guru for help, first parameter is seconds 15 | cron: 16 | collection: "0 0 0 * * *" 17 | 18 | management: 19 | endpoints: 20 | web: 21 | exposure: 22 | include: info,health,metrics,scheduledtasks,loggers,logfile,prometheus 23 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/service/ServiceInstanceMetricsService.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.service; 2 | 3 | import reactor.core.publisher.Flux; 4 | import reactor.core.publisher.Mono; 5 | import reactor.util.function.Tuple2; 6 | 7 | public interface ServiceInstanceMetricsService { 8 | 9 | Flux> byOrganization(); 10 | 11 | Flux> byService(); 12 | 13 | Flux> byServiceAndPlan(); 14 | 15 | Mono totalServiceInstances(); 16 | 17 | Flux> totalVelocity(); 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/test/java/org/cftoolsuite/cfapp/ButlerTest.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | import org.springframework.boot.test.context.SpringBootTest; 9 | import org.springframework.test.context.TestPropertySource; 10 | 11 | @Retention(RetentionPolicy.RUNTIME) 12 | @Target(ElementType.TYPE) 13 | @SpringBootTest 14 | @TestPropertySource(locations="classpath:/test.properties") 15 | public @interface ButlerTest { } 16 | -------------------------------------------------------------------------------- /src/test/resources/email-templates/email-with-attachments.json: -------------------------------------------------------------------------------- 1 | { 2 | "from": "sender@example.com", 3 | "recipients": ["recipient1@example.com"], 4 | "carbonCopyRecipients": [], 5 | "blindCarbonCopyRecipients": [], 6 | "subject": "Email with Attachments Test", 7 | "body": "This email contains attachments.", 8 | "domain": "example.com", 9 | "attachments": [ 10 | { 11 | "filename": "test", 12 | "extension": ".txt", 13 | "mimeType": "text/plain", 14 | "content": "VGhpcyBpcyBhIHRlc3QgYXR0YWNobWVudC4=" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/product/EulaAcceptance.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain.product; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 5 | 6 | import lombok.Builder; 7 | import lombok.Getter; 8 | 9 | @Builder 10 | @Getter 11 | @JsonPropertyOrder({ 12 | "href" 13 | }) 14 | public class EulaAcceptance { 15 | 16 | @JsonProperty("href") 17 | private String href; 18 | 19 | public EulaAcceptance(@JsonProperty("href") String href) { 20 | this.href = href; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/util/JarManifestUtil.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.util; 2 | 3 | import java.io.ByteArrayInputStream; 4 | import java.io.IOException; 5 | import java.util.jar.Attributes; 6 | import java.util.jar.Manifest; 7 | 8 | public class JarManifestUtil { 9 | 10 | public static String obtainAttributeValue(String contents, String key) throws IOException { 11 | Manifest manifest = new Manifest(new ByteArrayInputStream(contents.getBytes())); 12 | Attributes attributes = manifest.getMainAttributes(); 13 | return attributes.getValue(key); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/config/DbmsSettings.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.config; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.stereotype.Component; 5 | 6 | import io.r2dbc.spi.ConnectionFactory; 7 | 8 | @Component 9 | public class DbmsSettings { 10 | 11 | private final ConnectionFactory factory; 12 | 13 | @Autowired 14 | public DbmsSettings(ConnectionFactory factory) { 15 | this.factory = factory; 16 | } 17 | 18 | public String getProvider() { 19 | return factory.getMetadata().getName(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/service/AppRelationshipService.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.service; 2 | 3 | import org.cftoolsuite.cfapp.domain.AppRelationship; 4 | 5 | import reactor.core.publisher.Flux; 6 | import reactor.core.publisher.Mono; 7 | 8 | public interface AppRelationshipService { 9 | 10 | Mono deleteAll(); 11 | 12 | Flux findAll(); 13 | 14 | Flux findByApplicationId(String applicationId); 15 | 16 | Flux findByServiceInstanceId(String serviceInstanceId); 17 | 18 | Mono save(AppRelationship entity); 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/Href.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.Getter; 9 | 10 | @Builder 11 | @Getter 12 | @JsonPropertyOrder({ 13 | "href" 14 | }) 15 | public class Href { 16 | 17 | @JsonProperty("href") 18 | private String href; 19 | 20 | @JsonCreator 21 | public Href(@JsonProperty("href") String href) { 22 | this.href = href; 23 | } 24 | 25 | } -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/product/EulaLinks.java: -------------------------------------------------------------------------------- 1 | 2 | package org.cftoolsuite.cfapp.domain.product; 3 | 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 6 | 7 | import lombok.Builder; 8 | import lombok.Getter; 9 | 10 | 11 | @Builder 12 | @Getter 13 | @JsonPropertyOrder({ 14 | "self" 15 | }) 16 | public class EulaLinks { 17 | 18 | @JsonProperty("self") 19 | private Self self; 20 | 21 | public EulaLinks( 22 | @JsonProperty("self") Self self 23 | ) { 24 | this.self = self; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/service/UsageCache.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.service; 2 | 3 | import org.cftoolsuite.cfapp.domain.accounting.application.AppUsageReport; 4 | import org.cftoolsuite.cfapp.domain.accounting.service.ServiceUsageReport; 5 | import org.cftoolsuite.cfapp.domain.accounting.task.TaskUsageReport; 6 | import org.springframework.stereotype.Component; 7 | 8 | import lombok.Data; 9 | 10 | @Data 11 | @Component 12 | public class UsageCache { 13 | 14 | private AppUsageReport applicationReport; 15 | private ServiceUsageReport serviceReport; 16 | private TaskUsageReport taskReport; 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/config/OpsmanSettings.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.config; 2 | 3 | import org.cloudfoundry.uaa.tokens.GrantType; 4 | import org.springframework.boot.context.properties.ConfigurationProperties; 5 | 6 | import lombok.Data; 7 | 8 | @Data 9 | @ConfigurationProperties(prefix = "om") 10 | public class OpsmanSettings { 11 | 12 | private String apiHost; 13 | private String clientId = "opsman"; 14 | private String clientSecret = ""; 15 | private String username; 16 | private String password; 17 | private GrantType grantType = GrantType.PASSWORD; 18 | private boolean enabled; 19 | 20 | } 21 | -------------------------------------------------------------------------------- /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.springframework.data.annotation.Id; 6 | import org.springframework.data.annotation.PersistenceCreator; 7 | import org.springframework.data.relational.core.mapping.Table; 8 | 9 | import lombok.Getter; 10 | 11 | @Getter 12 | @Table("time_keeper") 13 | public class TimeKeeper { 14 | 15 | @Id 16 | private LocalDateTime collectionTime; 17 | 18 | @PersistenceCreator 19 | public TimeKeeper(LocalDateTime collectionTime) { 20 | this.collectionTime = collectionTime; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/product/Self.java: -------------------------------------------------------------------------------- 1 | 2 | package org.cftoolsuite.cfapp.domain.product; 3 | 4 | import com.fasterxml.jackson.annotation.JsonCreator; 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 7 | 8 | import lombok.Builder; 9 | import lombok.Getter; 10 | 11 | @Builder 12 | @Getter 13 | @JsonPropertyOrder({ 14 | "href" 15 | }) 16 | public class Self { 17 | 18 | @JsonProperty("href") 19 | private String href; 20 | 21 | @JsonCreator 22 | public Self(@JsonProperty("href") String href) { 23 | this.href = href; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Gradle ### 2 | .gradle 3 | bin/ 4 | tmp/ 5 | 6 | ### Maven ### 7 | target/ 8 | 9 | ### STS ### 10 | .apt_generated 11 | .classpath 12 | .factorypath 13 | .project 14 | .settings 15 | .springBeans 16 | 17 | ### IntelliJ IDEA ### 18 | .idea 19 | *.iws 20 | *.iml 21 | *.ipr 22 | 23 | ### NetBeans ### 24 | nbproject/private/ 25 | build/ 26 | nbbuild/ 27 | dist/ 28 | nbdist/ 29 | .nb-gradle/ 30 | logs/ 31 | 32 | ### Visual Studio Code ### 33 | .vscode 34 | .history/ 35 | 36 | ### User config 37 | config/*.json 38 | config/*.sql 39 | config/application-*.yml 40 | src/main/resources/application-*.yml 41 | infer-out 42 | 43 | ### MacOS 44 | .DS_Store 45 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/util/DbmsOnlyCondition.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.util; 2 | 3 | import org.springframework.context.annotation.Condition; 4 | import org.springframework.context.annotation.ConditionContext; 5 | import org.springframework.core.env.Environment; 6 | import org.springframework.core.type.AnnotatedTypeMetadata; 7 | 8 | 9 | public class DbmsOnlyCondition implements Condition { 10 | 11 | @Override 12 | public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { 13 | Environment env = context.getEnvironment(); 14 | return null == env.getProperty("cf.policies.git.uri"); 15 | } 16 | } -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/product/FileGroups.java: -------------------------------------------------------------------------------- 1 | 2 | package org.cftoolsuite.cfapp.domain.product; 3 | 4 | import com.fasterxml.jackson.annotation.JsonCreator; 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 7 | 8 | import lombok.Builder; 9 | import lombok.Getter; 10 | 11 | @Builder 12 | @Getter 13 | @JsonPropertyOrder({ 14 | "href" 15 | }) 16 | public class FileGroups { 17 | 18 | @JsonProperty("href") 19 | private String href; 20 | 21 | @JsonCreator 22 | public FileGroups(@JsonProperty("href") String href) { 23 | this.href = href; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/product/ProductFiles.java: -------------------------------------------------------------------------------- 1 | 2 | package org.cftoolsuite.cfapp.domain.product; 3 | 4 | import com.fasterxml.jackson.annotation.JsonCreator; 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 7 | 8 | import lombok.Builder; 9 | import lombok.Getter; 10 | 11 | @Builder 12 | @Getter 13 | @JsonPropertyOrder({ 14 | "href" 15 | }) 16 | public class ProductFiles { 17 | 18 | @JsonProperty("href") 19 | private String href; 20 | 21 | @JsonCreator 22 | public ProductFiles(@JsonProperty("href") String href) { 23 | this.href = href; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/service/JavaAppDetailService.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.service; 2 | 3 | import java.util.Map; 4 | 5 | import org.cftoolsuite.cfapp.domain.JavaAppDetail; 6 | 7 | import reactor.core.publisher.Flux; 8 | import reactor.core.publisher.Mono; 9 | 10 | public interface JavaAppDetailService { 11 | 12 | Mono deleteAll(); 13 | 14 | Flux findAll(); 15 | 16 | public Flux> findSpringApplications(); 17 | 18 | Mono> calculateSpringDependencyFrequency(); 19 | 20 | Mono findByAppId(String appId); 21 | 22 | Mono save(JavaAppDetail entity); 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/product/ArtifactReferences.java: -------------------------------------------------------------------------------- 1 | 2 | package org.cftoolsuite.cfapp.domain.product; 3 | 4 | import com.fasterxml.jackson.annotation.JsonCreator; 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 7 | 8 | import lombok.Builder; 9 | import lombok.Getter; 10 | 11 | @Builder 12 | @Getter 13 | @JsonPropertyOrder({ 14 | "href" 15 | }) 16 | public class ArtifactReferences { 17 | 18 | @JsonProperty("href") 19 | private String href; 20 | 21 | @JsonCreator 22 | public ArtifactReferences(@JsonProperty("href") String href) { 23 | this.href = href; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/event/PoliciesLoadedEvent.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.event; 2 | 3 | import org.cftoolsuite.cfapp.domain.Policies; 4 | import org.springframework.context.ApplicationEvent; 5 | 6 | public class PoliciesLoadedEvent extends ApplicationEvent { 7 | 8 | private static final long serialVersionUID = 1L; 9 | 10 | private Policies policies; 11 | 12 | public PoliciesLoadedEvent(Object source) { 13 | super(source); 14 | } 15 | 16 | public Policies getPolicies() { 17 | return policies; 18 | } 19 | 20 | public PoliciesLoadedEvent policies(Policies policies) { 21 | this.policies = policies; 22 | return this; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/event/SpacesRetrievedEvent.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.event; 2 | 3 | import java.util.List; 4 | 5 | import org.cftoolsuite.cfapp.domain.Space; 6 | import org.springframework.context.ApplicationEvent; 7 | 8 | public class SpacesRetrievedEvent extends ApplicationEvent { 9 | 10 | private static final long serialVersionUID = 1L; 11 | 12 | private List spaces; 13 | 14 | public SpacesRetrievedEvent(Object source) { 15 | super(source); 16 | } 17 | 18 | public List getSpaces() { 19 | return spaces; 20 | } 21 | 22 | public SpacesRetrievedEvent spaces(List spaces) { 23 | this.spaces = spaces; 24 | return this; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/event/TkRetrievedEvent.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.event; 2 | 3 | import java.time.LocalDateTime; 4 | 5 | import org.springframework.context.ApplicationEvent; 6 | 7 | public class TkRetrievedEvent extends ApplicationEvent { 8 | 9 | private static final long serialVersionUID = 1L; 10 | 11 | private LocalDateTime lastCollected; 12 | 13 | public TkRetrievedEvent(Object source) { 14 | super(source); 15 | } 16 | 17 | public LocalDateTime getLastCollected() { 18 | return lastCollected; 19 | } 20 | 21 | public TkRetrievedEvent lastCollected(LocalDateTime lastCollected) { 22 | this.lastCollected = lastCollected; 23 | return this; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/event/AppDetailRetrievedEvent.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.event; 2 | 3 | import java.util.List; 4 | 5 | import org.cftoolsuite.cfapp.domain.AppDetail; 6 | import org.springframework.context.ApplicationEvent; 7 | 8 | public class AppDetailRetrievedEvent extends ApplicationEvent { 9 | 10 | private static final long serialVersionUID = 1L; 11 | 12 | private List detail; 13 | 14 | public AppDetailRetrievedEvent(Object source) { 15 | super(source); 16 | } 17 | 18 | public AppDetailRetrievedEvent detail(List detail) { 19 | this.detail = detail; 20 | return this; 21 | } 22 | 23 | public List getDetail() { 24 | return detail; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/SnapshotSummary.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 5 | 6 | import lombok.Builder; 7 | import lombok.Getter; 8 | import lombok.ToString; 9 | 10 | @Builder 11 | @Getter 12 | @ToString 13 | @JsonPropertyOrder({ "application-counts", "service-instance-counts", "user-counts" }) 14 | public class SnapshotSummary { 15 | 16 | @JsonProperty("application-counts") 17 | private ApplicationCounts applicationCounts; 18 | 19 | @JsonProperty("service-instance-counts") 20 | private ServiceInstanceCounts serviceInstanceCounts; 21 | 22 | @JsonProperty("user-counts") 23 | private UserCounts userCounts; 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/test/java/org/cftoolsuite/cfapp/domain/OrganizationTest.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertTrue; 5 | 6 | import java.util.HashSet; 7 | import java.util.List; 8 | import java.util.Set; 9 | 10 | import org.junit.jupiter.api.Test; 11 | 12 | public class OrganizationTest { 13 | 14 | @Test 15 | public void assertThatOrganizationsAreEqual() { 16 | Organization org1 = new Organization("000eaf", "zoo-labs"); 17 | Organization org2 = new Organization("000eaf", "zoo-labs"); 18 | assertTrue(org1.equals(org2)); 19 | Set orgs = new HashSet<>(List.of(org1, org2)); 20 | assertEquals(orgs.size(), 1); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/Buildpack.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain; 2 | 3 | import lombok.Builder; 4 | import lombok.Getter; 5 | import lombok.ToString; 6 | 7 | @Builder 8 | @Getter 9 | @ToString 10 | public class Buildpack { 11 | 12 | private String id; 13 | private String name; 14 | private Integer position; 15 | private Boolean enabled; 16 | private Boolean locked; 17 | private String filename; 18 | private String stack; 19 | 20 | public String getVersion() { 21 | String version = null; 22 | int versionPosition = filename.lastIndexOf('v'); 23 | if (versionPosition >= 0) { 24 | version = filename.substring(versionPosition).replace(".zip", ""); 25 | } 26 | return version; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/event/UserAccountsRetrievedEvent.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.event; 2 | 3 | import java.util.List; 4 | 5 | import org.cftoolsuite.cfapp.domain.UserAccounts; 6 | import org.springframework.context.ApplicationEvent; 7 | 8 | public class UserAccountsRetrievedEvent extends ApplicationEvent { 9 | 10 | private static final long serialVersionUID = 1L; 11 | 12 | private List detail; 13 | 14 | public UserAccountsRetrievedEvent(Object source) { 15 | super(source); 16 | } 17 | 18 | public UserAccountsRetrievedEvent detail(List detail) { 19 | this.detail = detail; 20 | return this; 21 | } 22 | 23 | public List getDetail() { 24 | return detail; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /docs/CREDITS.md: -------------------------------------------------------------------------------- 1 | # VMware Tanzu Application Service > Butler 2 | 3 | ## Credits 4 | 5 | * [Oleh Dokuka](https://github.com/OlegDokuka) for writing [Hands-on Reactive Programming in Spring 5](https://www.packtpub.com/application-development/hands-reactive-programming-spring-5); it really helped level-up my understanding and practice on more than a few occasions 6 | * [Stephane Maldini](https://github.com/smaldini) for all the coaching on [Reactor](https://projectreactor.io); especially error handling 7 | * [Mark Paluch](https://github.com/mp911de) for coaching on [R2DBC](https://r2dbc.io) and helping me untangle dependencies 8 | * [Peter Royal](https://github.com/osi) for [assistance](https://gitter.im/reactor/reactor?at=5c38c24966f3433023afceb2) troubleshooting some design and implementation of policy execution tasks 9 | 10 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/service/AppDetailService.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.service; 2 | 3 | import java.time.LocalDate; 4 | 5 | import org.cftoolsuite.cfapp.domain.AppDetail; 6 | import org.cftoolsuite.cfapp.domain.ApplicationPolicy; 7 | 8 | import reactor.core.publisher.Flux; 9 | import reactor.core.publisher.Mono; 10 | import reactor.util.function.Tuple2; 11 | 12 | public interface AppDetailService { 13 | 14 | Mono deleteAll(); 15 | 16 | Flux findAll(); 17 | 18 | Mono findByAppId(String appId); 19 | 20 | Flux> findByApplicationPolicy(ApplicationPolicy policy, boolean mayHaveServiceBindings); 21 | 22 | Flux findByDateRange(LocalDate start, LocalDate end); 23 | 24 | Mono save(AppDetail entity); 25 | } 26 | -------------------------------------------------------------------------------- /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.autoconfigure.SpringBootApplication; 5 | import org.springframework.boot.context.properties.ConfigurationPropertiesScan; 6 | import org.springframework.transaction.annotation.EnableTransactionManagement; 7 | 8 | import io.netty.util.ResourceLeakDetector; 9 | import reactor.core.publisher.Hooks; 10 | 11 | 12 | @EnableTransactionManagement 13 | @ConfigurationPropertiesScan 14 | @SpringBootApplication 15 | public class AppInit { 16 | 17 | public static void main(String[] args) { 18 | Hooks.onOperatorDebug(); 19 | ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.ADVANCED); 20 | SpringApplication.run(AppInit.class, args); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/product/Releases.java: -------------------------------------------------------------------------------- 1 | 2 | package org.cftoolsuite.cfapp.domain.product; 3 | 4 | import java.util.ArrayList; 5 | import java.util.List; 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.Builder; 12 | import lombok.Builder.Default; 13 | import lombok.Getter; 14 | 15 | @Builder 16 | @Getter 17 | @JsonPropertyOrder({ 18 | "releases" 19 | }) 20 | public class Releases { 21 | 22 | @Default 23 | @JsonProperty("releases") 24 | private List releases = new ArrayList<>(); 25 | 26 | @JsonCreator 27 | public Releases(@JsonProperty("releases") List releases) { 28 | this.releases = releases; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/event/HistoricalRecordRetrievedEvent.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.event; 2 | 3 | import java.util.List; 4 | 5 | import org.cftoolsuite.cfapp.domain.HistoricalRecord; 6 | import org.springframework.context.ApplicationEvent; 7 | 8 | public class HistoricalRecordRetrievedEvent extends ApplicationEvent { 9 | 10 | private static final long serialVersionUID = 1L; 11 | 12 | private List records; 13 | 14 | public HistoricalRecordRetrievedEvent(Object source) { 15 | super(source); 16 | } 17 | 18 | public List getRecords() { 19 | return records; 20 | } 21 | 22 | public HistoricalRecordRetrievedEvent records(List records) { 23 | this.records = records; 24 | return this; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/product/ProductMetrics.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain.product; 2 | 3 | import java.util.Set; 4 | import java.util.TreeSet; 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({ "product-metrics "}) 17 | public class ProductMetrics { 18 | 19 | @Default 20 | @JsonProperty("product-metrics") 21 | Set productMetrics = new TreeSet<>(); 22 | 23 | @JsonCreator 24 | public ProductMetrics(Set productMetrics) { 25 | this.productMetrics = productMetrics; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/product/Products.java: -------------------------------------------------------------------------------- 1 | 2 | package org.cftoolsuite.cfapp.domain.product; 3 | 4 | import java.util.ArrayList; 5 | import java.util.List; 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.Builder; 12 | import lombok.Builder.Default; 13 | import lombok.Getter; 14 | 15 | 16 | @Builder 17 | @Getter 18 | @JsonPropertyOrder({ 19 | "products" 20 | }) 21 | public class Products { 22 | 23 | @Default 24 | @JsonProperty("products") 25 | private List products = new ArrayList<>(); 26 | 27 | @JsonCreator 28 | public Products(@JsonProperty("products") List products) { 29 | this.products = products; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/event/AppRelationshipRetrievedEvent.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.event; 2 | 3 | import java.util.List; 4 | 5 | import org.cftoolsuite.cfapp.domain.AppRelationship; 6 | import org.springframework.context.ApplicationEvent; 7 | 8 | public class AppRelationshipRetrievedEvent extends ApplicationEvent { 9 | 10 | private static final long serialVersionUID = 1L; 11 | 12 | private List relations; 13 | 14 | public AppRelationshipRetrievedEvent(Object source) { 15 | super(source); 16 | } 17 | 18 | public List getRelations() { 19 | return relations; 20 | } 21 | 22 | public AppRelationshipRetrievedEvent relations(List relations) { 23 | this.relations = relations; 24 | return this; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/util/DropletProcessingCondition.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.util; 2 | 3 | import java.util.Set; 4 | 5 | import org.springframework.context.annotation.Condition; 6 | import org.springframework.context.annotation.ConditionContext; 7 | import org.springframework.core.env.Environment; 8 | import org.springframework.core.type.AnnotatedTypeMetadata; 9 | 10 | 11 | public class DropletProcessingCondition implements Condition { 12 | 13 | @Override 14 | public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { 15 | Set activationValues = Set.of("unpack-pom-contents-in-droplet", "list-jars-in-droplet"); 16 | Environment env = context.getEnvironment(); 17 | return activationValues.contains(env.getProperty("java.artifacts.fetch.mode","")); 18 | } 19 | } -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/event/OrganizationsRetrievedEvent.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.event; 2 | 3 | import java.util.List; 4 | 5 | import org.cftoolsuite.cfapp.domain.Organization; 6 | import org.springframework.context.ApplicationEvent; 7 | 8 | public class OrganizationsRetrievedEvent extends ApplicationEvent { 9 | 10 | private static final long serialVersionUID = 1L; 11 | 12 | private List organizations; 13 | 14 | public OrganizationsRetrievedEvent(Object source) { 15 | super(source); 16 | } 17 | 18 | public List getOrganizations() { 19 | return organizations; 20 | } 21 | 22 | public OrganizationsRetrievedEvent organizations(List organizations) { 23 | this.organizations = organizations; 24 | return this; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/util/JarSetFilterReaderCondition.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.util; 2 | 3 | import java.util.Set; 4 | 5 | import org.springframework.context.annotation.Condition; 6 | import org.springframework.context.annotation.ConditionContext; 7 | import org.springframework.core.env.Environment; 8 | import org.springframework.core.type.AnnotatedTypeMetadata; 9 | 10 | 11 | public class JarSetFilterReaderCondition implements Condition { 12 | 13 | @Override 14 | public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { 15 | Set activationValues = Set.of("list-jars-in-droplet", "obtain-jars-from-runtime-metadata"); 16 | Environment env = context.getEnvironment(); 17 | return activationValues.contains(env.getProperty("java.artifacts.fetch.mode","")); 18 | } 19 | } -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/product/ProductType.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain.product; 2 | 3 | import com.fasterxml.jackson.annotation.JsonValue; 4 | 5 | public enum ProductType { 6 | BUILDPACK("buildpack"), 7 | STEMCELL("stemcell"), 8 | TILE("tile"); 9 | 10 | public static ProductType from(String value) { 11 | ProductType result = ProductType.TILE; 12 | if (value.contains("stemcell")) { 13 | result = ProductType.STEMCELL; 14 | } else if (value.contains("buildpack")) { 15 | result = ProductType.BUILDPACK; 16 | } 17 | return result; 18 | } 19 | 20 | private String id; 21 | 22 | ProductType(String id) { 23 | this.id = id; 24 | } 25 | 26 | @JsonValue 27 | public String getId() { 28 | return id; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/product/StemcellDetail.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain.product; 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.Getter; 9 | 10 | @Builder 11 | @Getter 12 | @JsonPropertyOrder({ "os", "version" }) 13 | public class StemcellDetail { 14 | 15 | @JsonProperty("os") 16 | private String os; 17 | 18 | @JsonProperty("version") 19 | private String version; 20 | 21 | @JsonCreator 22 | public StemcellDetail( 23 | @JsonProperty("os") String os, 24 | @JsonProperty("version") String version 25 | ) { 26 | this.os = os; 27 | this.version = version; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/service/ServiceInstanceDetailService.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.service; 2 | 3 | import java.time.LocalDate; 4 | 5 | import org.cftoolsuite.cfapp.domain.ServiceInstanceDetail; 6 | import org.cftoolsuite.cfapp.domain.ServiceInstancePolicy; 7 | 8 | import reactor.core.publisher.Flux; 9 | import reactor.core.publisher.Mono; 10 | import reactor.util.function.Tuple2; 11 | 12 | public interface ServiceInstanceDetailService { 13 | 14 | Mono deleteAll(); 15 | 16 | Flux findAll(); 17 | 18 | Flux findByDateRange(LocalDate start, LocalDate end); 19 | 20 | Flux> findByServiceInstancePolicy(ServiceInstancePolicy policy); 21 | 22 | Mono save(ServiceInstanceDetail entity); 23 | 24 | } 25 | -------------------------------------------------------------------------------- /scripts/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | export APP_NAME=cf-butler 6 | 7 | 8 | 9 | case "$1" in 10 | 11 | --with-credhub | -c) 12 | cf push --no-start 13 | cf create-service credhub default $APP_NAME-secrets -c config/secrets.json 14 | while [[ $(cf service $APP_NAME-secrets) != *"succeeded"* ]]; do 15 | echo "$APP_NAME-secrets is not ready yet..." 16 | sleep 5 17 | done 18 | cf bind-service $APP_NAME $APP_NAME-secrets 19 | cf start $APP_NAME 20 | ;; 21 | 22 | _ | *) 23 | cf push --no-start 24 | cf create-user-provided-service $APP_NAME-secrets -p config/secrets.json 25 | while [[ $(cf service $APP_NAME-secrets) != *"succeeded"* ]]; do 26 | echo "$APP_NAME-secrets is not ready yet..." 27 | sleep 5 28 | done 29 | cf bind-service $APP_NAME $APP_NAME-secrets 30 | cf start $APP_NAME 31 | ;; 32 | 33 | esac 34 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/event/ServiceInstanceDetailRetrievedEvent.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.event; 2 | 3 | import java.util.List; 4 | 5 | import org.cftoolsuite.cfapp.domain.ServiceInstanceDetail; 6 | import org.springframework.context.ApplicationEvent; 7 | 8 | public class ServiceInstanceDetailRetrievedEvent extends ApplicationEvent { 9 | 10 | private static final long serialVersionUID = 1L; 11 | 12 | private List detail; 13 | 14 | public ServiceInstanceDetailRetrievedEvent(Object source) { 15 | super(source); 16 | } 17 | 18 | public ServiceInstanceDetailRetrievedEvent detail(List detail) { 19 | this.detail = detail; 20 | return this; 21 | } 22 | 23 | public List getDetail() { 24 | return detail; 25 | } 26 | 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/test/java/org/cftoolsuite/cfapp/service/ReportRequestSpec.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.service; 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.Getter; 9 | 10 | 11 | @Builder 12 | @Getter 13 | @JsonPropertyOrder({ "input", "output" }) 14 | public class ReportRequestSpec { 15 | 16 | @JsonProperty("input") 17 | private final ReportRequest[] input; 18 | @JsonProperty("output") 19 | private final String output; 20 | 21 | @JsonCreator 22 | public ReportRequestSpec( 23 | @JsonProperty("input") ReportRequest[] input, 24 | @JsonProperty("output") String output) { 25 | this.input = input; 26 | this.output = output; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/config/JacksonDeSerConfig.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.config; 2 | 3 | import java.time.LocalDate; 4 | 5 | import org.cftoolsuite.cfapp.deser.RelaxedLocalDateDeserializer; 6 | import org.springframework.boot.jackson.autoconfigure.JsonMapperBuilderCustomizer; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | 10 | import tools.jackson.databind.module.SimpleModule; 11 | 12 | @Configuration 13 | public class JacksonDeSerConfig { 14 | 15 | @Bean 16 | public JsonMapperBuilderCustomizer addCustomDeserialization() { 17 | return builder -> { 18 | SimpleModule module = new SimpleModule(); 19 | module.addDeserializer(LocalDate.class, new RelaxedLocalDateDeserializer()); 20 | builder.addModule(module); 21 | }; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/Metadata.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonIgnore; 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | 7 | import lombok.Builder; 8 | import lombok.Getter; 9 | 10 | @Builder 11 | @Getter 12 | public class Metadata { 13 | 14 | @JsonProperty("metadata") 15 | private EmbeddedMetadata metadata; 16 | 17 | @JsonCreator 18 | public Metadata( 19 | @JsonProperty("metadata") EmbeddedMetadata metadata 20 | ) { 21 | this.metadata = metadata; 22 | } 23 | 24 | @JsonIgnore 25 | public boolean isValid() { 26 | if (metadata == null) { 27 | return true; 28 | } else { 29 | return metadata.isValid(); 30 | } 31 | 32 | 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/product/StemcellAssignments.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain.product; 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.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({ 17 | "products" 18 | }) 19 | public class StemcellAssignments { 20 | 21 | @Default 22 | @JsonProperty("products") 23 | private List products = new ArrayList<>(); 24 | 25 | @JsonCreator 26 | public StemcellAssignments(@JsonProperty("products") List products) { 27 | this.products = products; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/product/StemcellAssociations.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain.product; 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.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({ 17 | "products" 18 | }) 19 | public class StemcellAssociations { 20 | 21 | @Default 22 | @JsonProperty("products") 23 | private List products = new ArrayList<>(); 24 | 25 | @JsonCreator 26 | public StemcellAssociations(@JsonProperty("products") List products) { 27 | this.products = products; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /scripts/deploy.ps1: -------------------------------------------------------------------------------- 1 | #!\usr\bin\env pwsh 2 | 3 | param ( 4 | [string]$Provider = "--with-user-provided-service" 5 | ) 6 | 7 | $AppName="cf-butler" 8 | 9 | 10 | 11 | switch ($Provider) { 12 | 13 | "--with-credhub" { 14 | cf push --no-start 15 | cf create-service credhub default $AppName-secrets -c config\secrets.json 16 | while (cf service $AppName-secrets -notcontains "succeeded") { 17 | Write-Host "$APP_NAME-secrets is not ready yet..." 18 | Start-Sleep -Seconds 5 19 | } 20 | cf bind-service $AppName $AppName-secrets 21 | cf start $AppName 22 | } 23 | 24 | "--with-user-provided-service" { 25 | cf push --no-start 26 | cf create-user-provided-service $AppName-secrets -p config\secrets.json 27 | while (cf service $AppName-secrets -notcontains "succeeded") { 28 | Write-Host "$APP_NAME-secrets is not ready yet..." 29 | Start-Sleep -Seconds 5 30 | } 31 | cf bind-service $AppName $AppName-secrets 32 | cf start $AppName 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/service/AppMetricsService.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.service; 2 | 3 | import reactor.core.publisher.Flux; 4 | import reactor.core.publisher.Mono; 5 | import reactor.util.function.Tuple2; 6 | 7 | public interface AppMetricsService { 8 | 9 | Flux> byBuildpack(); 10 | 11 | Flux> byDockerImage(); 12 | 13 | Flux> byOrganization(); 14 | 15 | Flux> byStack(); 16 | 17 | Flux> byStatus(); 18 | 19 | Mono totalApplicationInstances(); 20 | 21 | Mono totalApplications(); 22 | 23 | Mono totalCrashedApplicationInstances(); 24 | 25 | Mono totalDiskUsed(); 26 | 27 | Mono totalMemoryUsed(); 28 | 29 | Mono totalRunningApplicationInstances(); 30 | 31 | Mono totalStoppedApplicationInstances(); 32 | 33 | Flux> totalVelocity(); 34 | 35 | } 36 | -------------------------------------------------------------------------------- /samples/secrets.pws.json: -------------------------------------------------------------------------------- 1 | { 2 | "PIVNET_API-TOKEN": "xxxxxx", 3 | "CF_TOKEN-PROVIDER": "userpass", 4 | "CF_API-HOST": "api.run.pivotal.io", 5 | "CF_USERNAME": "pwsaccount@youware.io", 6 | "CF_PASSWORD": "xxxxxx", 7 | "CF_ORGANIZATION-BLACK-LIST": [ "system" ], 8 | "CF_ACCOUNT-REGEX": "^[_A-Za-z0-9-\\+]+(\\.[_A-Za-z0-9-]+)*@[A-Za-z0-9-]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$", 9 | "CRON_COLLECTION": "0 0 0 * * *", 10 | "CF_POLICIES_GIT_URI": "https://github.com/cf-toolsuite/cf-butler-sample-config.git", 11 | "CF_POLICIES_GIT_COMMIT": "e745cfe5d93e6517038675fd6b0fe3b85524f130", 12 | "CF_POLICIES_GIT_FILE-PATHS": [ 13 | "delete-applications-policy-sample-AP.json", 14 | "delete-service-instances-policy-sample-SIP.json", 15 | "scale-applications-policy-sample-AP.json", 16 | "change-stack-applications-policy-sample-AP.json" 17 | ], 18 | "EXPOSED_ACTUATOR_ENDPOINTS": "beans,env,info,health,metrics,scheduledtasks,loggers,mappings,prometheus" 19 | } -------------------------------------------------------------------------------- /scripts/process-java-app-dependencies-tarball.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Function to check if Maven is installed 4 | check_maven() { 5 | if ! command -v mvn &> /dev/null; then 6 | echo "Maven is required but not installed. Please install Maven and re-run this script." 7 | exit 1 8 | fi 9 | } 10 | 11 | check_maven 12 | 13 | # Check if an argument is provided 14 | if [ "$#" -ne 1 ]; then 15 | echo "Please specify the path to a .tar.gz file to process." 16 | exit 1 17 | fi 18 | 19 | TAR_FILE=$1 20 | 21 | # Unpack the tar.gz file 22 | tar -xvf "$TAR_FILE" 23 | 24 | # Find all directories containing a pom.xml file and run the command in them 25 | find . -type f -name "pom.xml" | while read -r pom; do 26 | DIR=$(dirname "$pom") 27 | pushd "$DIR" > /dev/null || exit 28 | echo "Gathering dependencies within $DIR" 29 | mvn dependency:tree | grep -E '(org.springframework|io.micrometer)' > spring-dependencies.txt 30 | popd > /dev/null || exit 31 | done 32 | 33 | echo "Processing complete." 34 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/event/Resource.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain.event; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonInclude; 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 7 | 8 | import lombok.Builder; 9 | import lombok.Getter; 10 | 11 | @Builder 12 | @Getter 13 | @JsonInclude(JsonInclude.Include.NON_NULL) 14 | @JsonPropertyOrder({ 15 | "entity", 16 | "metadata" 17 | }) 18 | public class Resource { 19 | 20 | @JsonProperty("entity") 21 | private Entity entity; 22 | 23 | @JsonProperty("metadata") 24 | private ResourceMetadata metadata; 25 | 26 | @JsonCreator 27 | public Resource( 28 | @JsonProperty("entity") Entity entity, 29 | @JsonProperty("metadata") ResourceMetadata metadata 30 | ) { 31 | this.entity = entity; 32 | this.metadata = metadata; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/Resources.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain; 2 | 3 | 4 | import java.util.ArrayList; 5 | import java.util.List; 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.Builder; 12 | import lombok.Builder.Default; 13 | import lombok.Getter; 14 | 15 | @Builder 16 | @Getter 17 | @JsonPropertyOrder({ "resources"}) 18 | public class Resources { 19 | 20 | @Default 21 | @JsonProperty("resources") 22 | private List resources = new ArrayList<>(); 23 | 24 | @JsonProperty("pagination") 25 | private Pagination pagination; 26 | 27 | @JsonCreator 28 | public Resources( @JsonProperty("resources") List resources, 29 | @JsonProperty("pagination") Pagination pagination 30 | ) { 31 | this.resources = resources; 32 | this.pagination = pagination; 33 | } 34 | } -------------------------------------------------------------------------------- /.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.11/apache-maven-3.9.11-bin.zip 20 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/service/SpaceUsersService.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.service; 2 | 3 | import java.util.Map; 4 | 5 | import org.cftoolsuite.cfapp.domain.SpaceUsers; 6 | import org.cftoolsuite.cfapp.domain.UserAccounts; 7 | 8 | import reactor.core.publisher.Flux; 9 | import reactor.core.publisher.Mono; 10 | 11 | public interface SpaceUsersService { 12 | 13 | Mono> countByOrganization(); 14 | 15 | Mono deleteAll(); 16 | 17 | Flux findAll(); 18 | 19 | Flux findByAccountName(String name); 20 | 21 | Mono findByOrganizationAndSpace(String organization, String space); 22 | 23 | Flux obtainAccountNames(); 24 | 25 | Flux obtainServiceAccountNames(); 26 | 27 | Flux obtainUserAccountNames(); 28 | 29 | Flux obtainUserAccounts(); 30 | 31 | Mono save(SpaceUsers entity); 32 | 33 | Mono totalServiceAccounts(); 34 | 35 | Mono totalUserAccounts(); 36 | 37 | } 38 | -------------------------------------------------------------------------------- /scripts/deploy.cf4k8s.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | export APP_NAME=cf-butler 6 | 7 | if [ -z "$1" ] && [ -z "$2" ]; then 8 | echo "Usage: deploy.cf4k8s.sh {credential_store_provider_option} {path_to_secrets_file}" 9 | exit 1 10 | fi 11 | 12 | 13 | 14 | case "$1" in 15 | 16 | --with-credhub | -c) 17 | cf push -f manifest.cf4k8s.yml --no-start 18 | cf create-service credhub default $APP_NAME-secrets -c "$2" 19 | while [[ $(cf service $APP_NAME-secrets) != *"succeeded"* ]]; do 20 | echo "$APP_NAME-secrets is not ready yet..." 21 | sleep 5 22 | done 23 | cf bind-service $APP_NAME $APP_NAME-secrets 24 | cf start $APP_NAME 25 | ;; 26 | 27 | _ | *) 28 | cf push -f manifest.cf4k8s.yml --no-start 29 | cf create-user-provided-service $APP_NAME-secrets -p "$2" 30 | while [[ $(cf service $APP_NAME-secrets) != *"succeeded"* ]]; do 31 | echo "$APP_NAME-secrets is not ready yet..." 32 | sleep 5 33 | done 34 | cf bind-service $APP_NAME $APP_NAME-secrets 35 | cf start $APP_NAME 36 | ;; 37 | 38 | esac 39 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/product/Staleness.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain.product; 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.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({ 17 | "parent_products_deployed_more_recently" 18 | }) 19 | public class Staleness { 20 | 21 | @Default 22 | @JsonProperty("parent_products_deployed_more_recently") 23 | private List parentProductsDeployedMoreRecently = new ArrayList<>(); 24 | 25 | @JsonCreator 26 | public Staleness( 27 | @JsonProperty("parent_products_deployed_more_recently") List parentProductsDeployedMoreRecently 28 | ) { 29 | this.parentProductsDeployedMoreRecently = parentProductsDeployedMoreRecently; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /scripts/deploy.mysql.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Script assumes MySQL (https://network.pivotal.io/products/pivotal-mysql/) is available as service in cf marketplace 4 | # Feel free to swap out the service for other MySQL providers, like: 5 | # * Meta Azure Service Broker - https://github.com/Azure/meta-azure-service-broker/blob/master/docs/azure-mysql-db.md 6 | # * AWS Service Broker - http://docs.pivotal.io/aws-services/creating.html#rds 7 | 8 | set -e 9 | 10 | export APP_NAME=cf-butler 11 | 12 | 13 | cf push --no-start 14 | cf create-service credhub default $APP_NAME-secrets -c config/secrets.json 15 | cf create-service p.mysql db-small $APP_NAME-backend 16 | while [[ $(cf service $APP_NAME-secrets) != *"succeeded"* ]]; do 17 | echo "$APP_NAME-secrets is not ready yet..." 18 | sleep 5 19 | done 20 | cf bind-service $APP_NAME $APP_NAME-secrets 21 | while [[ $(cf service $APP_NAME-backend) != *"succeeded"* ]]; do 22 | echo "$APP_NAME-backend is not ready yet..." 23 | sleep 5 24 | done 25 | cf bind-service $APP_NAME $APP_NAME-backend 26 | cf start $APP_NAME 27 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/service/TkServiceUtil.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.service; 2 | 3 | import java.time.LocalDateTime; 4 | 5 | import org.springframework.http.HttpHeaders; 6 | 7 | import reactor.core.publisher.Mono; 8 | 9 | public class TkServiceUtil { 10 | 11 | private static final String LAST_TIME_COLLECTED = "X-DateTime-Collected"; 12 | 13 | private final TimeKeeperService tkService; 14 | 15 | public TkServiceUtil(TimeKeeperService tkService) { 16 | this.tkService = tkService; 17 | } 18 | 19 | public Mono getHeaders() { 20 | return 21 | tkService 22 | .findOne() 23 | .map(lc -> { 24 | HttpHeaders headers = new HttpHeaders(); 25 | headers.add(LAST_TIME_COLLECTED, lc.toString()); 26 | return headers; 27 | }); 28 | } 29 | 30 | public Mono getTimeCollected() { 31 | return 32 | tkService 33 | .findOne(); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /docs/PREREQUISITES.md: -------------------------------------------------------------------------------- 1 | # VMware Tanzu Application Service > Butler 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 | * and [admin credentials](https://docs.cloudfoundry.org/uaa/uaa-user-management.html#creating-admin-users) 9 | 10 | or 11 | 12 | * Access to a foundation with [VMware Tanzu Application Service](https://tanzu.vmware.com/platform/vmware-tanzu-application-service) 4.0.19+LTS-T or better installed 13 | * and VMware Tanzu Application Service [admin credentials](https://docs.vmware.com/en/VMware-Tanzu-Application-Service/4.0/tas-for-vms/uaa-user-management.html#create-an-admin-user-0) 14 | 15 | Optional 16 | 17 | * [VMware Tanzu Network](https://network.pivotal.io) account credentials 18 | * [VMware Tanzu Operations Manager](https://tanzu.vmware.com/platform/pcf-components/pcf-ops-manager) admin account credentials (or client id and client secret) 19 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/repository/R2dbcQueryRepository.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.repository; 2 | 3 | import org.cftoolsuite.cfapp.domain.Query; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.data.r2dbc.core.R2dbcEntityOperations; 6 | import org.springframework.stereotype.Repository; 7 | 8 | import io.r2dbc.spi.Row; 9 | import io.r2dbc.spi.RowMetadata; 10 | import reactor.core.publisher.Flux; 11 | import reactor.util.function.Tuple2; 12 | import reactor.util.function.Tuples; 13 | 14 | @Repository 15 | public class R2dbcQueryRepository { 16 | 17 | private final R2dbcEntityOperations client; 18 | 19 | @Autowired 20 | public R2dbcQueryRepository(R2dbcEntityOperations client) { 21 | this.client = client; 22 | } 23 | 24 | public Flux> executeQuery(Query query) { 25 | return 26 | client 27 | .getDatabaseClient() 28 | .sql(query.getSql()) 29 | .map(Tuples::of) 30 | .all(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/service/AccountMatcher.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.service; 2 | 3 | import java.util.regex.Matcher; 4 | import java.util.regex.Pattern; 5 | 6 | import org.cftoolsuite.cfapp.config.PasSettings; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.stereotype.Component; 9 | 10 | import lombok.extern.slf4j.Slf4j; 11 | 12 | @Slf4j 13 | @Component 14 | public class AccountMatcher { 15 | 16 | private final Pattern pattern; 17 | private final PasSettings settings; 18 | 19 | @Autowired 20 | public AccountMatcher(PasSettings settings) { 21 | this.settings = settings; 22 | this.pattern = Pattern.compile(settings.getAccountRegex()); 23 | } 24 | 25 | public boolean matches(final String candidate) { 26 | Matcher matcher = pattern.matcher(candidate); 27 | boolean result = matcher.matches(); 28 | log.trace("Does account {} match account regex pattern {}? {}", candidate, settings.getAccountRegex(), String.valueOf(result)); 29 | return result; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/EmailAttachment.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | 5 | import lombok.Builder; 6 | import lombok.Getter; 7 | 8 | @Builder 9 | @Getter 10 | public class EmailAttachment { 11 | 12 | private final String headers; 13 | private final String content; 14 | private final String filename; 15 | private final String mimeType; 16 | private final String extension; 17 | 18 | public String getHeadedContent() { 19 | StringBuilder result = new StringBuilder(); 20 | if (hasHeaders()) { 21 | result.append(getHeaders()); 22 | result.append(System.getProperty("line.separator")); 23 | } 24 | if (hasContent()) { 25 | result.append(getContent()); 26 | } 27 | return result.toString(); 28 | } 29 | 30 | public boolean hasContent() { 31 | return StringUtils.isNotBlank(getContent()); 32 | } 33 | 34 | public boolean hasHeaders() { 35 | return StringUtils.isNotBlank(getHeaders()); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/ApplicationState.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain; 2 | 3 | import java.util.Arrays; 4 | import java.util.List; 5 | import java.util.stream.Collectors; 6 | 7 | import org.springframework.util.Assert; 8 | 9 | import com.fasterxml.jackson.annotation.JsonValue; 10 | 11 | public enum ApplicationState { 12 | 13 | STARTED("started"), 14 | STOPPED("stopped"); 15 | 16 | public static ApplicationState from(String name) { 17 | Assert.hasText(name, "ApplicationState must not be null or empty"); 18 | List states = Arrays.asList(ApplicationState.values()); 19 | ApplicationState result = states.stream().filter(s -> s.getName().equalsIgnoreCase(name)).collect(Collectors.toList()).get(0); 20 | Assert.notNull(result, String.format("Invalid ApplicationState, name=%s", name)); 21 | return result; 22 | } 23 | 24 | private String name; 25 | 26 | ApplicationState(String name) { 27 | this.name = name; 28 | } 29 | 30 | @JsonValue 31 | public String getName() { 32 | return name; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/test/java/org/cftoolsuite/cfapp/repository/R2dbcTimeKeeperRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.repository; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import java.time.LocalDateTime; 6 | 7 | import org.cftoolsuite.cfapp.ButlerTest; 8 | import org.junit.jupiter.api.Test; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | 11 | import reactor.test.StepVerifier; 12 | 13 | @ButlerTest 14 | public class R2dbcTimeKeeperRepositoryTest { 15 | 16 | private final R2dbcTimeKeeperRepository repo; 17 | 18 | @Autowired 19 | public R2dbcTimeKeeperRepositoryTest( 20 | R2dbcTimeKeeperRepository repo 21 | ) { 22 | this.repo = repo; 23 | } 24 | 25 | @Test 26 | public void testSaveWasSuccessful() { 27 | LocalDateTime now = LocalDateTime.now(); 28 | StepVerifier.create( 29 | repo.deleteOne() 30 | .then(repo.save(now)) 31 | .then(repo.findOne())) 32 | .assertNext(one -> { 33 | assertEquals(now, one); 34 | }).verifyComplete(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/SpaceUsersReadConverter.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain; 2 | 3 | import org.springframework.core.convert.converter.Converter; 4 | import org.springframework.data.convert.ReadingConverter; 5 | import org.springframework.stereotype.Indexed; 6 | 7 | import io.r2dbc.spi.Row; 8 | 9 | @Indexed 10 | @ReadingConverter 11 | public class SpaceUsersReadConverter implements Converter { 12 | 13 | @Override 14 | public SpaceUsers convert(Row source) { 15 | return 16 | SpaceUsers 17 | .builder() 18 | .pk(source.get("pk", Long.class)) 19 | .organization(Defaults.getColumnValue(source, "organization", String.class)) 20 | .space(Defaults.getColumnValue(source, "space", String.class)) 21 | .auditors(Defaults.getColumnListOfStringValue(source, "auditors")) 22 | .developers(Defaults.getColumnListOfStringValue(source, "developers")) 23 | .managers(Defaults.getColumnListOfStringValue(source, "managers")) 24 | .build(); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /.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: [ 25 ] 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 targeting H2 backend 27 | run: ./mvnw --batch-mode --update-snapshots -Plog4j2 clean verify 28 | - name: Build with Maven targeting MySQL backend 29 | run: ./mvnw --batch-mode --update-snapshots -Drdbms=mysql -Plog4j2 clean verify 30 | - name: Build with Maven targeting Postgresql backend 31 | run: ./mvnw --batch-mode --update-snapshots -Drdbms=postgres -Plog4j2 clean verify 32 | -------------------------------------------------------------------------------- /samples/secrets.pws.with-postgres.json: -------------------------------------------------------------------------------- 1 | { 2 | "PIVNET_API-TOKEN": "xxxxxx", 3 | "CF_TOKEN-PROVIDER": "userpass", 4 | "CF_API-HOST": "api.run.pivotal.io", 5 | "CF_USERNAME": "pwsaccount@youware.io", 6 | "CF_PASSWORD": "replace_me", 7 | "CF_ORGANIZATION-BLACK-LIST": [ "system" ], 8 | "CF_ACCOUNT-REGEX": "^[_A-Za-z0-9-\\+]+(\\.[_A-Za-z0-9-]+)*@[A-Za-z0-9-]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$", 9 | "CRON_COLLECTION": "0 0 0 * * *", 10 | "CF_POLICIES_GIT_URI": "https://github.com/cf-toolsuite/cf-butler-sample-config.git", 11 | "CF_POLICIES_GIT_COMMIT": "e745cfe5d93e6517038675fd6b0fe3b85524f130", 12 | "CF_POLICIES_GIT_FILE-PATHS": [ 13 | "delete-applications-policy-sample-AP.json", 14 | "delete-service-instances-policy-sample-SIP.json", 15 | "scale-applications-policy-sample-AP.json", 16 | "change-stack-applications-policy-sample-AP.json" 17 | ], 18 | "EXPOSED_ACTUATOR_ENDPOINTS": "beans,env,info,health,metrics,scheduledtasks,loggers,mappings,prometheus", 19 | "R2DBC_URL": "r2dbc:postgresql://:/", 20 | "R2DBC_USERNAME": "", 21 | "R2DBC_PASSWORD": "" 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/util/CsvUtil.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.util; 2 | 3 | import java.io.IOException; 4 | import java.io.StringReader; 5 | import java.util.HashSet; 6 | import java.util.List; 7 | import java.util.Set; 8 | 9 | import org.apache.commons.csv.CSVFormat; 10 | import org.apache.commons.csv.CSVParser; 11 | import org.apache.commons.csv.CSVRecord; 12 | import org.apache.commons.lang3.StringUtils; 13 | 14 | import lombok.extern.slf4j.Slf4j; 15 | 16 | @Slf4j 17 | public class CsvUtil { 18 | 19 | public static Set parse(String csvInput) { 20 | Set result = new HashSet<>(); 21 | if (StringUtils.isNotBlank(csvInput)) { 22 | try { 23 | CSVParser csvParser = CSVParser.parse(new StringReader(csvInput), CSVFormat.DEFAULT); 24 | for (CSVRecord csvRecord : csvParser) { 25 | List value = csvRecord.toList(); 26 | result.addAll(value); 27 | } 28 | } catch (IOException e) { 29 | log.error("Error parsing CSV input", e); 30 | } 31 | } 32 | return result; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /scripts/deploy.postgres.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Script assumes VMware Postgres for VMware Tanzu Application Service (https://docs.vmware.com/en/VMware-Postgres-for-VMware-Tanzu-Application-Service/1.1/postgres/index.html) is available as service in cf marketplace 4 | # Feel free to swap out the service for other PostgreSQL providers, like: 5 | # * Meta Azure Service Broker - https://github.com/Azure/meta-azure-service-broker/blob/master/docs/azure-postgresql-db.md 6 | # * AWS Service Broker - http://docs.pivotal.io/aws-services/creating.html#rds 7 | 8 | set -e 9 | 10 | export APP_NAME=cf-butler 11 | 12 | 13 | cf push --no-start 14 | cf create-user-provided-service $APP_NAME-secrets -p config/secrets.json 15 | cf create-service postgres on-demand-postgres-db $APP_NAME-backend 16 | while [[ $(cf service $APP_NAME-secrets) != *"succeeded"* ]]; do 17 | echo "$APP_NAME-secrets is not ready yet..." 18 | sleep 5 19 | done 20 | cf bind-service $APP_NAME $APP_NAME-secrets 21 | while [[ $(cf service $APP_NAME-backend) != *"succeeded"* ]]; do 22 | echo "$APP_NAME-backend is not ready yet..." 23 | sleep 5 24 | done 25 | cf bind-service $APP_NAME $APP_NAME-backend 26 | cf start $APP_NAME 27 | -------------------------------------------------------------------------------- /samples/secrets.pws.with-mysql.json: -------------------------------------------------------------------------------- 1 | { 2 | "PIVNET_API-TOKEN": "xxxxxx", 3 | "CF_TOKEN-PROVIDER": "userpass", 4 | "CF_API-HOST": "api.run.pivotal.io", 5 | "CF_USERNAME": "pwsaccount@youware.io", 6 | "CF_PASSWORD": "replace_me", 7 | "CF_ORGANIZATION-BLACK-LIST": [ "system" ], 8 | "CF_ACCOUNT-REGEX": "^[_A-Za-z0-9-\\+]+(\\.[_A-Za-z0-9-]+)*@[A-Za-z0-9-]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$", 9 | "CRON_COLLECTION": "0 0 0 * * *", 10 | "CF_POLICIES_GIT_URI": "https://github.com/cf-toolsuite/cf-butler-sample-config.git", 11 | "CF_POLICIES_GIT_COMMIT": "e745cfe5d93e6517038675fd6b0fe3b85524f130", 12 | "CF_POLICIES_GIT_FILE-PATHS": [ 13 | "delete-applications-policy-sample-AP.json", 14 | "delete-service-instances-policy-sample-SIP.json", 15 | "scale-applications-policy-sample-AP.json", 16 | "change-stack-applications-policy-sample-AP.json", 17 | "query-policy-mysql-sample-QP.json" 18 | ], 19 | "EXPOSED_ACTUATOR_ENDPOINTS": "beans,env,info,health,metrics,scheduledtasks,loggers,mappings,prometheus", 20 | "R2DBC_URL": "r2dbc:mysql://:/", 21 | "R2DBC_USERNAME": "", 22 | "R2DBC_PASSWORD": "" 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/controller/OnDemandCollectorTriggerController.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.controller; 2 | 3 | import org.cftoolsuite.cfapp.task.ProductsAndReleasesTask; 4 | import org.cftoolsuite.cfapp.task.TkTask; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.context.annotation.Profile; 7 | import org.springframework.http.ResponseEntity; 8 | import org.springframework.web.bind.annotation.PostMapping; 9 | import org.springframework.web.bind.annotation.RestController; 10 | 11 | import reactor.core.publisher.Mono; 12 | 13 | @Profile("on-demand") 14 | @RestController 15 | public class OnDemandCollectorTriggerController { 16 | 17 | @Autowired 18 | private TkTask tkCollector; 19 | 20 | @Autowired(required = false) 21 | private ProductsAndReleasesTask productsAndReleasesCollector; 22 | 23 | @PostMapping("/collect") 24 | public Mono> triggerCollection() { 25 | tkCollector.collect(); 26 | if (productsAndReleasesCollector != null) { 27 | productsAndReleasesCollector.collect(); 28 | } 29 | return Mono.just(ResponseEntity.accepted().build()); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/Organization.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain; 2 | 3 | import org.springframework.data.annotation.Id; 4 | import org.springframework.data.annotation.PersistenceCreator; 5 | import org.springframework.data.relational.core.mapping.Column; 6 | import org.springframework.data.relational.core.mapping.Table; 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.EqualsAndHashCode; 13 | import lombok.Getter; 14 | import lombok.ToString; 15 | 16 | @Getter 17 | @EqualsAndHashCode 18 | @JsonPropertyOrder({ "id", "name"}) 19 | @ToString 20 | @Table("organizations") 21 | public class Organization { 22 | 23 | @Id 24 | @JsonProperty("id") 25 | private final String id; 26 | 27 | @Column("org_name") 28 | @JsonProperty("name") 29 | private final String name; 30 | 31 | 32 | @JsonCreator 33 | @PersistenceCreator 34 | public Organization( 35 | @JsonProperty("id") String id, 36 | @JsonProperty("name") String name) { 37 | this.id = id; 38 | this.name = name; 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/service/UserSpacesService.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.service; 2 | 3 | import org.cftoolsuite.cfapp.domain.Space; 4 | import org.cftoolsuite.cfapp.domain.UserSpaces; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.stereotype.Service; 7 | 8 | import reactor.core.publisher.Mono; 9 | 10 | @Service 11 | public class UserSpacesService { 12 | 13 | private static Space buildSpace(String organization, String space) { 14 | return Space 15 | .builder() 16 | .organizationName(organization) 17 | .spaceName(space) 18 | .build(); 19 | } 20 | 21 | private final SpaceUsersService service; 22 | 23 | @Autowired 24 | public UserSpacesService(SpaceUsersService service) { 25 | this.service = service; 26 | } 27 | 28 | public Mono getUserSpaces(String name) { 29 | return service 30 | .findByAccountName(name) 31 | .map(su -> buildSpace(su.getOrganization(), su.getSpace())) 32 | .collectList() 33 | .map(spaces -> UserSpaces.builder().accountName(name).spaces(spaces).build()); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /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.Map; 5 | 6 | import com.fasterxml.jackson.annotation.JsonProperty; 7 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 8 | 9 | import lombok.Builder; 10 | import lombok.Builder.Default; 11 | import lombok.Getter; 12 | import lombok.ToString; 13 | 14 | @Builder 15 | @Getter 16 | @ToString 17 | @JsonPropertyOrder({ "by-organization", "by-service", "by-service-and-plan", "total-service-instances", "velocity" }) 18 | public class ServiceInstanceCounts { 19 | 20 | @Default 21 | @JsonProperty("by-organization") 22 | private Map byOrganization = new HashMap<>(); 23 | 24 | @Default 25 | @JsonProperty("by-service") 26 | private Map byService = new HashMap<>(); 27 | 28 | @Default 29 | @JsonProperty("by-service-and-plan") 30 | private Map byServiceAndPlan = new HashMap<>(); 31 | 32 | @Default 33 | @JsonProperty("total-service-instances") 34 | private Long totalServiceInstances = 0L; 35 | 36 | @Default 37 | @JsonProperty("velocity") 38 | private Map velocity = new HashMap<>(); 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/service/ReportRequest.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.service; 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.Getter; 9 | 10 | @Builder 11 | @Getter 12 | @JsonPropertyOrder({ "foundation", "environment", "period", "filename" }) 13 | class ReportRequest { 14 | 15 | @JsonProperty("foundation") 16 | private final String foundation; 17 | @JsonProperty("environment") 18 | private final String environment; 19 | @JsonProperty("period") 20 | private final String period; 21 | @JsonProperty("filename") 22 | private final String filename; 23 | 24 | @JsonCreator 25 | public ReportRequest( 26 | @JsonProperty("foundation") String foundation, 27 | @JsonProperty("environment") String environment, 28 | @JsonProperty("period") String period, 29 | @JsonProperty("filename") String filename) { 30 | this.foundation = foundation; 31 | this.environment = environment; 32 | this.period = period; 33 | this.filename = filename; 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/product/ProductLinks.java: -------------------------------------------------------------------------------- 1 | 2 | package org.cftoolsuite.cfapp.domain.product; 3 | 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 6 | 7 | import lombok.Builder; 8 | import lombok.Getter; 9 | 10 | 11 | @Builder 12 | @Getter 13 | @JsonPropertyOrder({ 14 | "self", 15 | "releases", 16 | "product_files", 17 | "file_groups" 18 | }) 19 | public class ProductLinks { 20 | 21 | @JsonProperty("self") 22 | private Self self; 23 | 24 | @JsonProperty("releases") 25 | private Releases releases; 26 | 27 | @JsonProperty("product_files") 28 | private ProductFiles productFiles; 29 | 30 | @JsonProperty("file_groups") 31 | private FileGroups fileGroups; 32 | 33 | public ProductLinks( 34 | @JsonProperty("self") Self self, 35 | @JsonProperty("releases") Releases releases, 36 | @JsonProperty("product_files") ProductFiles productFiles, 37 | @JsonProperty("file_groups") FileGroups fileGroups 38 | ) { 39 | this.self = self; 40 | this.releases = releases; 41 | this.productFiles = productFiles; 42 | this.fileGroups = fileGroups; 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/repository/R2dbcTimeKeeperRepository.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.repository; 2 | 3 | import java.time.LocalDateTime; 4 | 5 | import org.cftoolsuite.cfapp.domain.TimeKeeper; 6 | import org.springframework.data.r2dbc.core.R2dbcEntityOperations; 7 | import org.springframework.stereotype.Repository; 8 | 9 | import reactor.core.publisher.Mono; 10 | 11 | @Repository 12 | public class R2dbcTimeKeeperRepository { 13 | 14 | private final R2dbcEntityOperations client; 15 | 16 | public R2dbcTimeKeeperRepository(R2dbcEntityOperations client) { 17 | this.client = client; 18 | } 19 | 20 | public Mono deleteOne() { 21 | return 22 | client 23 | .delete(TimeKeeper.class) 24 | .all() 25 | .then(); 26 | } 27 | 28 | public Mono findOne() { 29 | return 30 | client 31 | .select(TimeKeeper.class) 32 | .first() 33 | .map(TimeKeeper::getCollectionTime); 34 | } 35 | 36 | public Mono save(LocalDateTime collectionTime) { 37 | return 38 | client 39 | .insert(new TimeKeeper(collectionTime)); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/deser/RelaxedLocalDateDeserializer.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.deser; 2 | 3 | import java.time.LocalDate; 4 | import java.time.format.DateTimeParseException; 5 | 6 | import org.apache.commons.lang3.StringUtils; 7 | 8 | import tools.jackson.core.JacksonException; 9 | import tools.jackson.core.JsonParser; 10 | import tools.jackson.databind.DeserializationContext; 11 | import tools.jackson.databind.ValueDeserializer; 12 | 13 | public class RelaxedLocalDateDeserializer extends ValueDeserializer { 14 | 15 | @Override 16 | public LocalDate deserialize(JsonParser p, DeserializationContext ctxt) throws JacksonException { 17 | try { 18 | String dateStr = p.getText(); 19 | return LocalDate.parse(dateStr); 20 | } catch (DateTimeParseException e) { 21 | try { 22 | String date = p.getText(); 23 | String[] iso8601DateParts = date.split("-"); 24 | if (StringUtils.isNotBlank(iso8601DateParts[0]) && iso8601DateParts[0].length() > 4) { 25 | return LocalDate.MAX; 26 | } 27 | } catch (JacksonException je) { 28 | throw je; 29 | } 30 | throw new RuntimeException(e); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VMware Tanzu Application Service > Butler 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-butler/actions/workflows/ci.yml/badge.svg) [![Known Vulnerabilities](https://snyk.io/test/github/cf-toolsuite/cf-butler/badge.svg?style=plastic)](https://snyk.io/test/github/cf-toolsuite/cf-butler) [![Release](https://jitpack.io/v/cf-toolsuite/cf-butler.svg)](https://jitpack.io/#cf-toolsuite/cf-butler/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 | * [Integrate w/ Operations Manager](docs/INTEGRATIONS.md) 15 | * [Define and manage policies](docs/POLICIES.md) 16 | * [Deploy to Tanzu Application Service](docs/TAS.md) 17 | * [Consume endpoints](docs/ENDPOINTS.md) 18 | * [Troubleshooting](docs/TROUBLESHOOTING.md) 19 | * Videos (coming soon) 20 | * [Credits](docs/CREDITS.md) 21 | -------------------------------------------------------------------------------- /docs/INTEGRATIONS.md: -------------------------------------------------------------------------------- 1 | # VMware Tanzu Application Service > Butler 2 | 3 | ## Integration w/ Operations Manager 4 | 5 | You must add the following configuration properties to `application-{env}.yml` if you want to enable integration with an Operations Manager instance 6 | 7 | * `om.apiHost` - a VMware Tanzu Operations Manager API endpoint 8 | * `om.enabled` - a boolean property that must be set to `true` 9 | * `om.grantType` - [Token](https://docs.cloudfoundry.org/api/uaa/version/75.4.0/index.html#token) grant type 10 | 11 | If `om.grantType` is set to `password` 12 | 13 | * `om.username` - username for Operations Manager admin account 14 | * `om.password` - password for Operations Manager admin account 15 | * `om.clientId` - must be set to `opsman` 16 | * `om.clientSecret` - must be set to blank 17 | 18 | If `om.grantType` is set to `client_credentials` 19 | 20 | * `om.username` - must be set to blank 21 | * `om.password` - must be set to blank 22 | * `om.clientId` - the recipient of the token 23 | * `om.clientSecret` - the secret passphrase configured for the OAuth client 24 | 25 | > the `{env}` filename suffix above denotes the Spring Profile you would activate for your environment 26 | 27 | or 28 | 29 | Add entries in your `config/secrets.json` like 30 | 31 | ``` 32 | "OM_API-HOST": "xxxxxx", 33 | "OM_ENABLED": true 34 | ``` -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/controller/ProductMetricsController.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.controller; 2 | 3 | import org.cftoolsuite.cfapp.domain.product.ProductMetrics; 4 | import org.cftoolsuite.cfapp.service.ProductMetricsService; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; 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 reactor.core.publisher.Mono; 12 | 13 | @RestController 14 | @ConditionalOnExpression( 15 | "${om.enabled:false} and ${pivnet.enabled:false}" 16 | ) 17 | public class ProductMetricsController { 18 | 19 | private final ProductMetricsService service; 20 | 21 | @Autowired 22 | public ProductMetricsController(ProductMetricsService service) { 23 | this.service = service; 24 | } 25 | 26 | @GetMapping("/products/metrics") 27 | public Mono> getProductMetrics() { 28 | return service 29 | .getProductMetrics() 30 | .map(ResponseEntity::ok) 31 | .defaultIfEmpty(ResponseEntity.notFound().build()); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/config/H2ConsoleConfig.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.config; 2 | 3 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 4 | import org.springframework.context.event.ContextClosedEvent; 5 | import org.springframework.context.event.ContextRefreshedEvent; 6 | import org.springframework.context.event.EventListener; 7 | import org.springframework.stereotype.Component; 8 | 9 | import lombok.extern.slf4j.Slf4j; 10 | 11 | @Slf4j 12 | @Component 13 | @ConditionalOnProperty(prefix = "spring.h2.console", name = "enabled", havingValue = "true", matchIfMissing = false) 14 | public class H2ConsoleConfig { 15 | 16 | private org.h2.tools.Server webServer; 17 | 18 | private org.h2.tools.Server server; 19 | 20 | @EventListener(ContextRefreshedEvent.class) 21 | public void start() throws java.sql.SQLException { 22 | log.trace("Enabling H2 Web console..."); 23 | this.webServer = org.h2.tools.Server.createWebServer("-webPort", "8082", "-tcpAllowOthers").start(); 24 | this.server = org.h2.tools.Server.createTcpServer("-tcpPort", "9092", "-tcpAllowOthers").start(); 25 | } 26 | 27 | @EventListener(ContextClosedEvent.class) 28 | public void stop() { 29 | this.webServer.stop(); 30 | this.server.stop(); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/CustomConverters.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain; 2 | 3 | import java.util.List; 4 | 5 | public class CustomConverters { 6 | 7 | public static List get() { 8 | return List.of( 9 | new AppDetailReadConverter(), 10 | new AppDetailWriteConverter(), 11 | new ApplicationPolicyReadConverter(), 12 | new ApplicationPolicyWriteConverter(), 13 | new EndpointPolicyReadConverter(), 14 | new EndpointPolicyWriteConverter(), 15 | new HygienePolicyReadConverter(), 16 | new HygienePolicyWriteConverter(), 17 | new LegacyPolicyReadConverter(), 18 | new LegacyPolicyWriteConverter(), 19 | new QueryPolicyReadConverter(), 20 | new QueryPolicyWriteConverter(), 21 | new ResourceNotificationPolicyReadConverter(), 22 | new ResourceNotificationPolicyWriteConverter(), 23 | new ServiceInstanceDetailReadConverter(), 24 | new ServiceInstanceDetailWriteConverter(), 25 | new ServiceInstancePolicyReadConverter(), 26 | new ServiceInstancePolicyWriteConverter(), 27 | new SpaceUsersReadConverter(), 28 | new SpaceUsersWriteConverter() 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/test/java/org/cftoolsuite/cfapp/repository/ListRemoteRepositoryExample.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.repository; 2 | 3 | import org.cftoolsuite.cfapp.client.GitClient; 4 | import org.cftoolsuite.cfapp.config.GitSettings; 5 | import org.eclipse.jgit.lib.Repository; 6 | 7 | public class ListRemoteRepositoryExample { 8 | 9 | private static final String REMOTE_URL_1 = "https://github.com/github/testrepo.git"; 10 | private static final String REMOTE_URL_2 = "https://github.com/cf-toolsuite/test-repo.git"; 11 | public static void main(String[] args) throws Exception { 12 | GitClient helper = new GitClient(); 13 | Repository repo = helper.getRepository(GitSettings.builder().uri(REMOTE_URL_1).build()); 14 | String advice = helper.readFile(repo, "26fc70913efc66a93fe84f8ce1bba09954624490", "test/advice.c"); 15 | System.out.println(advice); 16 | 17 | // This will intentionally fail b/c I'm not sharing credentials for a private repository 18 | // but it serves to demonstrate how one can retrieve file contests 19 | repo = helper.getRepository(GitSettings.builder().uri(REMOTE_URL_2).username("change_me").build()); 20 | String readme = helper.readFile(repo, "0f7aac949b32d7636f11918dc34ae8bb251fa610", "README.md"); 21 | System.out.println(readme); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/test/resources/access-filter.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": [ 3 | { 4 | "excludeClasses": "com.vaadin.external.google.**" 5 | }, 6 | { 7 | "excludeClasses": "org.apache.maven.surefire.**" 8 | }, 9 | { 10 | "excludeClasses": "net.bytebuddy.**" 11 | }, 12 | { 13 | "excludeClasses": "org.jayway.**" 14 | }, 15 | { 16 | "excludeClasses": "net.minidev.**" 17 | }, 18 | { 19 | "excludeClasses": "org.apiguardian.**" 20 | }, 21 | { 22 | "excludeClasses": "org.assertj.**" 23 | }, 24 | { 25 | "excludeClasses": "org.hamcrest.**" 26 | }, 27 | { 28 | "excludeClasses": "org.junit.**" 29 | }, 30 | { 31 | "excludeClasses": "org.mockito.**" 32 | }, 33 | { 34 | "excludeClasses": "org.objenesis.**" 35 | }, 36 | { 37 | "excludeClasses": "org.skyscreamer.**" 38 | }, 39 | { 40 | "excludeClasses": "org.springframework.test.**" 41 | }, 42 | { 43 | "excludeClasses": "org.springframework.boot.test.**" 44 | }, 45 | { 46 | "excludeClasses": "org.xmlunit.**" 47 | } 48 | ] 49 | } -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/service/R2dbcSpaceService.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.service; 2 | 3 | import org.cftoolsuite.cfapp.domain.Space; 4 | import org.cftoolsuite.cfapp.repository.R2dbcSpaceRepository; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.stereotype.Service; 7 | import org.springframework.transaction.annotation.Transactional; 8 | 9 | import lombok.extern.slf4j.Slf4j; 10 | import reactor.core.publisher.Flux; 11 | import reactor.core.publisher.Mono; 12 | 13 | @Slf4j 14 | @Service 15 | public class R2dbcSpaceService implements SpaceService { 16 | 17 | private final R2dbcSpaceRepository repo; 18 | 19 | @Autowired 20 | public R2dbcSpaceService(R2dbcSpaceRepository repo) { 21 | this.repo = repo; 22 | } 23 | 24 | @Override 25 | @Transactional 26 | public Mono deleteAll() { 27 | return repo.deleteAll(); 28 | } 29 | 30 | @Override 31 | public Flux findAll() { 32 | return repo.findAll(); 33 | } 34 | 35 | @Override 36 | @Transactional 37 | public Mono save(Space entity) { 38 | return repo 39 | .save(entity) 40 | .onErrorContinue( 41 | (ex, data) -> log.error(String.format("Problem saving space %s.", entity), ex)); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /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.JsonProperty; 9 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 10 | 11 | import lombok.Builder; 12 | import lombok.Builder.Default; 13 | import lombok.Getter; 14 | import lombok.ToString; 15 | 16 | @Builder 17 | @Getter 18 | @ToString 19 | @JsonPropertyOrder({ "applications", "service-instances", "application-relationships", "user-accounts", "service-accounts" }) 20 | public class SnapshotDetail { 21 | 22 | @Default 23 | @JsonProperty("applications") 24 | private List applications = new ArrayList<>(); 25 | 26 | @Default 27 | @JsonProperty("service-instances") 28 | private List serviceInstances = new ArrayList<>(); 29 | 30 | @Default 31 | @JsonProperty("application-relationships") 32 | private List applicationRelationships = new ArrayList<>(); 33 | 34 | @Default 35 | @JsonProperty("user-accounts") 36 | private Set userAccounts = new HashSet<>(); 37 | 38 | @Default 39 | @JsonProperty("service-accounts") 40 | private Set serviceAccounts = new HashSet<>(); 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/service/R2dbcQueryService.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.service; 2 | 3 | import org.cftoolsuite.cfapp.domain.Query; 4 | import org.cftoolsuite.cfapp.repository.R2dbcQueryRepository; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.stereotype.Service; 7 | import org.springframework.transaction.annotation.Transactional; 8 | 9 | import io.r2dbc.spi.Row; 10 | import io.r2dbc.spi.RowMetadata; 11 | import lombok.extern.slf4j.Slf4j; 12 | import reactor.core.publisher.Flux; 13 | import reactor.util.function.Tuple2; 14 | 15 | @Slf4j 16 | @Service 17 | public class R2dbcQueryService implements QueryService { 18 | 19 | private final R2dbcQueryRepository repo; 20 | 21 | @Autowired 22 | public R2dbcQueryService(R2dbcQueryRepository repo) { 23 | this.repo = repo; 24 | } 25 | 26 | @Override 27 | @Transactional 28 | public Flux> executeQuery(Query query) { 29 | log.trace(String.format("Attempting to execute a query named [ %s ] and the statement is [ %s ]", query.getName(), query.getSql())); 30 | return repo 31 | .executeQuery(query) 32 | .onErrorContinue( 33 | (ex, data) -> log.error(String.format("Problem executing query %s.", query.getSql()), ex)); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /scripts/deploy.alt.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | export APP_NAME=cf-butler 6 | 7 | if [ -z "$1" ] && [ -z "$2" ]; then 8 | echo "Usage: deploy.alt.sh {credential_store_provider_option} {path_to_secrets_file} {additional-cf-push-options}" 9 | exit 1 10 | fi 11 | 12 | 13 | 14 | case "$1" in 15 | 16 | --with-credhub | -c) 17 | cf push --no-start "$3" 18 | if ! cf service $APP_NAME-secrets > /dev/null; then 19 | cf create-service credhub default $APP_NAME-secrets -c "$2" 20 | for (( i = 0; i < 90; i++ )); do 21 | if [[ $(cf service $APP_NAME-secrets) != *"succeeded"* ]]; then 22 | echo "$APP_NAME-secrets is not ready yet..." 23 | sleep 10 24 | else 25 | break 26 | fi 27 | done 28 | fi 29 | cf bind-service $APP_NAME $APP_NAME-secrets 30 | cf start $APP_NAME 31 | ;; 32 | 33 | _ | *) 34 | cf push --no-start "$3" 35 | if ! cf service $APP_NAME-secrets > /dev/null; then 36 | cf create-user-provided-service $APP_NAME-secrets -p "$2" 37 | for (( i = 0; i < 90; i++ )); do 38 | if [[ $(cf service $APP_NAME-secrets) != *"succeeded"* ]]; then 39 | echo "$APP_NAME-secrets is not ready yet..." 40 | sleep 10 41 | else 42 | break 43 | fi 44 | done 45 | fi 46 | cf bind-service $APP_NAME $APP_NAME-secrets 47 | cf start $APP_NAME 48 | ;; 49 | 50 | esac 51 | -------------------------------------------------------------------------------- /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.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({ "usages", "service_plan_name", "service_plan_guid"}) 17 | public class ServicePlanUsageMonthly { 18 | 19 | @Default 20 | @JsonProperty("usages") 21 | public List usages = new ArrayList<>(); 22 | 23 | @JsonProperty("service_plan_name") 24 | public String servicePlanName; 25 | 26 | @JsonProperty("service_plan_guid") 27 | public String servicePlanGuid; 28 | 29 | @JsonCreator 30 | public ServicePlanUsageMonthly( 31 | @JsonProperty("usages") List usages, 32 | @JsonProperty("service_plan_name") String servicePlanName, 33 | @JsonProperty("service_plan_guid") String servicePlanGuid) { 34 | this.usages = usages; 35 | this.servicePlanName = servicePlanName; 36 | this.servicePlanGuid = servicePlanGuid; 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/EndpointRequest.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain; 2 | 3 | 4 | import com.fasterxml.jackson.annotation.JsonCreator; 5 | import com.fasterxml.jackson.annotation.JsonInclude; 6 | import com.fasterxml.jackson.annotation.JsonProperty; 7 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 8 | 9 | import lombok.Builder; 10 | import lombok.Getter; 11 | import lombok.ToString; 12 | 13 | @Builder 14 | @JsonInclude(JsonInclude.Include.NON_NULL) 15 | @JsonPropertyOrder({ "endpoint", "json-path-expression", "apply-json-to-csv-converter" }) 16 | @Getter 17 | @ToString 18 | public class EndpointRequest { 19 | 20 | @JsonProperty("endpoint") 21 | private String endpoint; 22 | 23 | @JsonProperty("json-path-expression") 24 | private String jsonPathExpression; 25 | 26 | @JsonProperty("apply-json-to-csv-converter") 27 | private boolean applyJsonToCsvConverter; 28 | 29 | @JsonCreator 30 | public EndpointRequest( 31 | @JsonProperty("endpoint") String endpoint, 32 | @JsonProperty("json-path-expression") String jsonPathExpression, 33 | @JsonProperty("apply-json-to-csv-converter") boolean applyJsonToCsvConverter 34 | ) { 35 | this.endpoint = endpoint; 36 | this.jsonPathExpression = jsonPathExpression; 37 | this.applyJsonToCsvConverter = applyJsonToCsvConverter; 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/event/ResourceMetadata.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain.event; 2 | 3 | import java.time.Instant; 4 | 5 | import com.fasterxml.jackson.annotation.JsonCreator; 6 | import com.fasterxml.jackson.annotation.JsonInclude; 7 | import com.fasterxml.jackson.annotation.JsonProperty; 8 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 9 | 10 | import lombok.Builder; 11 | import lombok.Getter; 12 | 13 | @Builder 14 | @Getter 15 | @JsonInclude(JsonInclude.Include.NON_NULL) 16 | @JsonPropertyOrder({ 17 | "created_at", 18 | "guid", 19 | "updated_at", 20 | "url" 21 | }) 22 | public class ResourceMetadata { 23 | 24 | @JsonProperty("created_at") 25 | private Instant createdAt; 26 | 27 | @JsonProperty("guid") 28 | private String guid; 29 | 30 | @JsonProperty("updated_at") 31 | private Instant updatedAt; 32 | 33 | @JsonProperty("url") 34 | private String url; 35 | 36 | @JsonCreator 37 | public ResourceMetadata( 38 | @JsonProperty("created_at") Instant createdAt, 39 | @JsonProperty("guid") String guid, 40 | @JsonProperty("updated_at") Instant updatedAt, 41 | @JsonProperty("url") String url) { 42 | this.createdAt = createdAt; 43 | this.guid = guid; 44 | this.updatedAt = updatedAt; 45 | this.url = url; 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/task/AppDetailReadyToBeCollectedDecider.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.task; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.concurrent.atomic.AtomicInteger; 6 | 7 | import org.cftoolsuite.cfapp.config.PivnetSettings; 8 | import org.cftoolsuite.cfapp.domain.Space; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.stereotype.Component; 11 | 12 | @Component 13 | public class AppDetailReadyToBeCollectedDecider { 14 | 15 | private PivnetSettings settings; 16 | private AtomicInteger decision = new AtomicInteger(); 17 | private List spaces = new ArrayList<>(); 18 | 19 | @Autowired 20 | public AppDetailReadyToBeCollectedDecider(PivnetSettings settings) { 21 | this.settings = settings; 22 | } 23 | 24 | public List getSpaces() { 25 | return List.copyOf(spaces); 26 | } 27 | 28 | public int informDecision() { 29 | return decision.incrementAndGet(); 30 | } 31 | 32 | public boolean isDecided() { 33 | return settings.isEnabled() ? decision.get() == 2: decision.get() == 1; 34 | } 35 | 36 | public void reset() { 37 | spaces.clear(); 38 | decision.set(0); 39 | } 40 | 41 | public void setSpaces(List spaces) { 42 | this.spaces = spaces; 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/report/HistoricalRecordCsvReport.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.report; 2 | 3 | import java.time.LocalDateTime; 4 | 5 | import org.cftoolsuite.cfapp.config.PasSettings; 6 | import org.cftoolsuite.cfapp.domain.HistoricalRecord; 7 | import org.cftoolsuite.cfapp.event.HistoricalRecordRetrievedEvent; 8 | 9 | public class HistoricalRecordCsvReport { 10 | 11 | private PasSettings settings; 12 | 13 | public HistoricalRecordCsvReport(PasSettings settings) { 14 | this.settings = settings; 15 | } 16 | 17 | public String generateDetail(HistoricalRecordRetrievedEvent event) { 18 | StringBuffer detail = new StringBuffer(); 19 | detail.append("\n"); 20 | detail.append(HistoricalRecord.headers()); 21 | detail.append("\n"); 22 | event.getRecords() 23 | .forEach(a -> { 24 | detail.append(a.toCsv()); 25 | detail.append("\n"); 26 | }); 27 | return detail.toString(); 28 | } 29 | 30 | public String generatePreamble() { 31 | StringBuffer preamble = new StringBuffer(); 32 | preamble.append("Historical records from "); 33 | preamble.append(settings.getApiHost()); 34 | preamble.append(" generated "); 35 | preamble.append(LocalDateTime.now()); 36 | preamble.append("."); 37 | return preamble.toString(); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/Resource.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain; 2 | 3 | import java.time.Instant; 4 | 5 | import com.fasterxml.jackson.annotation.JsonCreator; 6 | import com.fasterxml.jackson.annotation.JsonProperty; 7 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 8 | 9 | import lombok.Builder; 10 | import lombok.Getter; 11 | 12 | @Builder 13 | @Getter 14 | @JsonPropertyOrder({ "guid","name","created_at", "updated_at", "metadata" }) 15 | public class Resource { 16 | 17 | @JsonProperty("guid") 18 | private String guid; 19 | 20 | @JsonProperty("name") 21 | private String name; 22 | 23 | @JsonProperty("created_at") 24 | private Instant createdAt; 25 | 26 | @JsonProperty("updated_at") 27 | private Instant updatedAt; 28 | 29 | @JsonProperty("metadata") 30 | private EmbeddedMetadata metadata; 31 | 32 | @JsonCreator 33 | public Resource( 34 | 35 | @JsonProperty("guid") String guid, 36 | @JsonProperty("name") String name, 37 | @JsonProperty("created_at") Instant createdAt, 38 | @JsonProperty("updated_at") Instant updatedAt, 39 | @JsonProperty("metadata") EmbeddedMetadata metadata) { 40 | 41 | this.guid = guid; 42 | this.name = name; 43 | this.createdAt = createdAt; 44 | this.updatedAt = updatedAt; 45 | this.metadata = metadata; 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/product/Eula.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain.product; 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({ 14 | "id", 15 | "slug", 16 | "name", 17 | "_links", 18 | "archived_at" 19 | }) 20 | public class Eula { 21 | 22 | @Default 23 | @JsonProperty("id") 24 | private Long id = -1L; 25 | 26 | @JsonProperty("slug") 27 | private String slug; 28 | 29 | @JsonProperty("name") 30 | private String name; 31 | 32 | @JsonProperty("_links") 33 | private EulaLinks links; 34 | 35 | @JsonProperty 36 | private String archivedAt; 37 | 38 | @JsonCreator 39 | public Eula( 40 | @JsonProperty("id") Long id, 41 | @JsonProperty("slug") String slug, 42 | @JsonProperty("name") String name, 43 | @JsonProperty("_links") EulaLinks links, 44 | @JsonProperty("archived_at") String archivedAt 45 | ) { 46 | this.id = id; 47 | this.slug = slug; 48 | this.name = name; 49 | this.links = links; 50 | this.archivedAt = archivedAt; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/service/TimeKeeperService.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.service; 2 | 3 | import java.time.LocalDateTime; 4 | 5 | import org.cftoolsuite.cfapp.domain.TimeKeeper; 6 | import org.cftoolsuite.cfapp.repository.R2dbcTimeKeeperRepository; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.stereotype.Service; 9 | import org.springframework.transaction.annotation.Transactional; 10 | 11 | import lombok.extern.slf4j.Slf4j; 12 | import reactor.core.publisher.Mono; 13 | 14 | @Slf4j 15 | @Service 16 | public class TimeKeeperService { 17 | 18 | private final R2dbcTimeKeeperRepository repo; 19 | 20 | @Autowired 21 | public TimeKeeperService(R2dbcTimeKeeperRepository repo) { 22 | this.repo = repo; 23 | } 24 | 25 | @Transactional 26 | public Mono deleteOne() { 27 | return repo.deleteOne(); 28 | } 29 | 30 | public Mono findOne() { 31 | return repo.findOne(); 32 | } 33 | 34 | @Transactional 35 | public Mono save() { 36 | LocalDateTime collectionTime = LocalDateTime.now(); 37 | return 38 | repo 39 | .save(collectionTime) 40 | .onErrorContinue( 41 | (ex, data) -> log.error(String.format("Problem saving collectime time %s.", collectionTime), ex)); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /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/main/java/org/cftoolsuite/cfapp/domain/accounting/task/TaskUsageReport.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain.accounting.task; 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.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({"report_time", "monthly_reports", "yearly_reports"}) 17 | public class TaskUsageReport { 18 | 19 | @JsonProperty("report_time") 20 | private String reportTime; 21 | 22 | @Default 23 | @JsonProperty("monthly_reports") 24 | private List monthlyReports = new ArrayList<>(); 25 | 26 | @Default 27 | @JsonProperty("yearly_reports") 28 | private List yearlyReports = new ArrayList<>(); 29 | 30 | @JsonCreator 31 | public TaskUsageReport( 32 | @JsonProperty("report_time") String reportTime, 33 | @JsonProperty("monthly_reports") List monthlyReports, 34 | @JsonProperty("yearly_reports") List yearlyReports) { 35 | this.reportTime = reportTime; 36 | this.monthlyReports = monthlyReports; 37 | this.yearlyReports = yearlyReports; 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/util/RetryableTokenProvider.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.util; 2 | 3 | import java.time.Duration; 4 | 5 | import org.cloudfoundry.reactor.DefaultConnectionContext; 6 | import org.cloudfoundry.reactor.TokenProvider; 7 | import org.springframework.http.HttpStatus; 8 | import org.springframework.web.reactive.function.client.WebClientResponseException; 9 | 10 | import reactor.core.publisher.Mono; 11 | import reactor.util.retry.Retry; 12 | 13 | public class RetryableTokenProvider { 14 | 15 | public static Mono getToken(TokenProvider provider, DefaultConnectionContext connectionContext) { 16 | return Mono.defer(() -> 17 | provider.getToken(connectionContext) 18 | .onErrorResume(WebClientResponseException.class, throwable -> { 19 | if (throwable.getStatusCode() == HttpStatus.UNAUTHORIZED) { 20 | provider.invalidate(connectionContext); 21 | // Retry logic 22 | return Mono.error(throwable); // Propagate error to trigger retry 23 | } 24 | return Mono.error(throwable); // Propagate other errors 25 | }) 26 | .retryWhen(Retry 27 | .backoff(2, Duration.ofSeconds(2)) 28 | ) 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/controller/JavaAppDetailController.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.controller; 2 | 3 | import java.util.Map; 4 | 5 | import org.cftoolsuite.cfapp.service.JavaAppDetailService; 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 reactor.core.publisher.Flux; 12 | import reactor.core.publisher.Mono; 13 | 14 | 15 | @RestController 16 | public class JavaAppDetailController { 17 | 18 | private JavaAppDetailService service; 19 | 20 | @Autowired 21 | public JavaAppDetailController(JavaAppDetailService service) { 22 | this.service = service; 23 | } 24 | 25 | @GetMapping("/snapshot/detail/ai/spring") 26 | public ResponseEntity>> getSpringApplications() { 27 | return 28 | ResponseEntity 29 | .ok() 30 | .body(service.findSpringApplications()); 31 | } 32 | 33 | @GetMapping("/snapshot/summary/ai/spring") 34 | public ResponseEntity>> calculateSpringDependencyFrequency() { 35 | return 36 | ResponseEntity 37 | .ok() 38 | .body(service.calculateSpringDependencyFrequency()); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/accounting/application/AppUsageReport.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain.accounting.application; 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.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({"report_time", "monthly_reports", "yearly_reports"}) 17 | public class AppUsageReport { 18 | 19 | @JsonProperty("report_time") 20 | private String reportTime; 21 | 22 | @Default 23 | @JsonProperty("monthly_reports") 24 | private List monthlyReports = new ArrayList<>(); 25 | 26 | @Default 27 | @JsonProperty("yearly_reports") 28 | private List yearlyReports = new ArrayList<>(); 29 | 30 | @JsonCreator 31 | public AppUsageReport( 32 | @JsonProperty("report_time") String reportTime, 33 | @JsonProperty("monthly_reports") List monthlyReports, 34 | @JsonProperty("yearly_reports") List yearlyReports) { 35 | this.reportTime = reportTime; 36 | this.monthlyReports = monthlyReports; 37 | this.yearlyReports = yearlyReports; 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/notifier/AppDetailConsoleNotifier.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.notifier; 2 | 3 | import org.cftoolsuite.cfapp.config.PasSettings; 4 | import org.cftoolsuite.cfapp.event.AppDetailRetrievedEvent; 5 | import org.cftoolsuite.cfapp.report.AppDetailCsvReport; 6 | import org.cftoolsuite.cfapp.service.TimeKeeperService; 7 | import org.cftoolsuite.cfapp.service.TkServiceUtil; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.context.ApplicationListener; 10 | import org.springframework.stereotype.Component; 11 | 12 | import lombok.extern.slf4j.Slf4j; 13 | 14 | @Slf4j 15 | @Component 16 | public class AppDetailConsoleNotifier implements ApplicationListener { 17 | 18 | private final AppDetailCsvReport report; 19 | private final TkServiceUtil util; 20 | 21 | @Autowired 22 | public AppDetailConsoleNotifier( 23 | PasSettings appSettings, 24 | TimeKeeperService tkService) { 25 | this.report = new AppDetailCsvReport(appSettings); 26 | this.util = new TkServiceUtil(tkService); 27 | } 28 | 29 | @Override 30 | public void onApplicationEvent(AppDetailRetrievedEvent event) { 31 | util 32 | .getTimeCollected() 33 | .subscribe(tc -> log.trace(String.join("%n%n", report.generatePreamble(tc), report.generateDetail(event)))); 34 | 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/task/SpacesRetrievedListener.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.task; 2 | 3 | import org.cftoolsuite.cfapp.event.AppDetailReadyToBeRetrievedEvent; 4 | import org.cftoolsuite.cfapp.event.SpacesRetrievedEvent; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.context.ApplicationEventPublisher; 7 | import org.springframework.context.ApplicationListener; 8 | import org.springframework.stereotype.Component; 9 | 10 | @Component 11 | public class SpacesRetrievedListener implements ApplicationListener { 12 | 13 | private final ApplicationEventPublisher publisher; 14 | private final AppDetailReadyToBeCollectedDecider appDetailReadyToBeCollectedDecider; 15 | 16 | @Autowired 17 | public SpacesRetrievedListener( 18 | ApplicationEventPublisher publisher, 19 | AppDetailReadyToBeCollectedDecider appDetailReadyToBeCollectedDecider) { 20 | this.publisher = publisher; 21 | this.appDetailReadyToBeCollectedDecider = appDetailReadyToBeCollectedDecider; 22 | } 23 | 24 | @Override 25 | public void onApplicationEvent(SpacesRetrievedEvent event) { 26 | appDetailReadyToBeCollectedDecider.informDecision(); 27 | appDetailReadyToBeCollectedDecider.setSpaces(event.getSpaces()); 28 | publisher.publishEvent(new AppDetailReadyToBeRetrievedEvent(this)); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/task/ProductsAndReleasesRetrievedListener.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.task; 2 | 3 | import org.cftoolsuite.cfapp.event.AppDetailReadyToBeRetrievedEvent; 4 | import org.cftoolsuite.cfapp.event.ProductsAndReleasesRetrievedEvent; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.context.ApplicationEventPublisher; 7 | import org.springframework.context.ApplicationListener; 8 | import org.springframework.stereotype.Component; 9 | 10 | @Component 11 | public class ProductsAndReleasesRetrievedListener implements ApplicationListener { 12 | 13 | private final ApplicationEventPublisher publisher; 14 | private final AppDetailReadyToBeCollectedDecider appDetailReadyToBeCollectedDecider; 15 | 16 | @Autowired 17 | public ProductsAndReleasesRetrievedListener( 18 | ApplicationEventPublisher publisher, 19 | AppDetailReadyToBeCollectedDecider appDetailReadyToBeCollectedDecider) { 20 | this.publisher = publisher; 21 | this.appDetailReadyToBeCollectedDecider = appDetailReadyToBeCollectedDecider; 22 | } 23 | 24 | @Override 25 | public void onApplicationEvent(ProductsAndReleasesRetrievedEvent event) { 26 | appDetailReadyToBeCollectedDecider.informDecision(); 27 | publisher.publishEvent(new AppDetailReadyToBeRetrievedEvent(this)); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/service/R2dbcOrganizationService.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.service; 2 | 3 | import org.cftoolsuite.cfapp.domain.Organization; 4 | import org.cftoolsuite.cfapp.repository.R2dbcOrganizationRepository; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.stereotype.Service; 7 | import org.springframework.transaction.annotation.Transactional; 8 | 9 | import lombok.extern.slf4j.Slf4j; 10 | import reactor.core.publisher.Flux; 11 | import reactor.core.publisher.Mono; 12 | 13 | @Slf4j 14 | @Service 15 | public class R2dbcOrganizationService implements OrganizationService { 16 | 17 | private final R2dbcOrganizationRepository repo; 18 | 19 | @Autowired 20 | public R2dbcOrganizationService(R2dbcOrganizationRepository repo) { 21 | this.repo = repo; 22 | } 23 | 24 | @Override 25 | @Transactional 26 | public Mono deleteAll() { 27 | return repo.deleteAll(); 28 | } 29 | 30 | @Override 31 | public Flux findAll() { 32 | return repo.findAll(); 33 | } 34 | 35 | @Override 36 | @Transactional 37 | public Mono save(Organization entity) { 38 | return repo 39 | .save(entity) 40 | .onErrorContinue( 41 | (ex, data) -> log.error(String.format("Problem saving organization %s.", entity), ex)); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/Query.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | 5 | import com.fasterxml.jackson.annotation.JsonCreator; 6 | import com.fasterxml.jackson.annotation.JsonIgnore; 7 | import com.fasterxml.jackson.annotation.JsonInclude; 8 | import com.fasterxml.jackson.annotation.JsonProperty; 9 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 10 | 11 | import lombok.Builder; 12 | import lombok.Getter; 13 | 14 | @Builder 15 | @JsonInclude(JsonInclude.Include.NON_NULL) 16 | @JsonPropertyOrder({ "name", "description", "sql" }) 17 | @Getter 18 | public class Query { 19 | 20 | @JsonProperty("name") 21 | private String name; 22 | 23 | @JsonProperty("description") 24 | private String description; 25 | 26 | @JsonProperty("sql") 27 | private String sql; 28 | 29 | @JsonCreator 30 | public Query( 31 | @JsonProperty("name") String name, 32 | @JsonProperty("description") String description, 33 | @JsonProperty("sql") String sql 34 | ) { 35 | this.name = name; 36 | this.description = description; 37 | this.sql = sql; 38 | } 39 | 40 | @JsonIgnore 41 | public boolean isValid() { 42 | return StringUtils.isNotBlank(name) 43 | && StringUtils.isNotBlank(sql) 44 | && sql.toLowerCase().startsWith("select"); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/Pagination.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.Getter; 9 | 10 | @Builder 11 | @Getter 12 | @JsonPropertyOrder({ "pagination"}) 13 | public class Pagination { 14 | 15 | 16 | @JsonProperty("total_results") 17 | private Integer totalResult; 18 | 19 | @JsonProperty("total_pages") 20 | private Integer totalPages; 21 | 22 | 23 | @JsonProperty("next") 24 | private Href next; 25 | 26 | 27 | @JsonProperty("previous") 28 | private Href previous; 29 | 30 | 31 | @JsonProperty("first") 32 | private Href first; 33 | 34 | 35 | @JsonProperty("last") 36 | private Href last; 37 | 38 | @JsonCreator 39 | public Pagination(@JsonProperty("total_results") Integer totalResult, 40 | @JsonProperty("total_pages") Integer totalPages, 41 | @JsonProperty("previous") Href previous, 42 | @JsonProperty("next") Href next, 43 | @JsonProperty("first") Href first, 44 | @JsonProperty("last") Href last 45 | ) { 46 | this.totalResult = totalResult; 47 | this.totalPages = totalPages; 48 | this.previous = previous; 49 | this.next = next; 50 | this.first = first; 51 | this.last = last; 52 | } 53 | } -------------------------------------------------------------------------------- /src/test/java/org/cftoolsuite/cfapp/repository/R2dbcOrganizationRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.repository; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import java.util.UUID; 6 | 7 | import org.cftoolsuite.cfapp.ButlerTest; 8 | import org.cftoolsuite.cfapp.domain.Organization; 9 | import org.junit.jupiter.api.BeforeEach; 10 | import org.junit.jupiter.api.Test; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | 13 | import reactor.test.StepVerifier; 14 | 15 | @ButlerTest 16 | public class R2dbcOrganizationRepositoryTest { 17 | 18 | private final R2dbcOrganizationRepository repo; 19 | 20 | @Autowired 21 | public R2dbcOrganizationRepositoryTest( 22 | R2dbcOrganizationRepository repo 23 | ) { 24 | this.repo = repo; 25 | } 26 | 27 | @BeforeEach 28 | public void setup() { 29 | StepVerifier.create(repo.deleteAll()).verifyComplete(); 30 | } 31 | 32 | @Test 33 | public void testSaveWasSuccessful() { 34 | String id = UUID.randomUUID().toString(); 35 | String name = "zoo-labs"; 36 | Organization entity = new Organization(id, name); 37 | StepVerifier.create(repo.save(entity) 38 | .thenMany(repo.findAll())) 39 | .assertNext(o -> { 40 | assertEquals(id, o.getId()); 41 | assertEquals(name, o.getName()); 42 | }).verifyComplete(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/product/OmInfo.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain.product; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | 6 | import lombok.Builder; 7 | import lombok.Getter; 8 | 9 | @Builder 10 | @Getter 11 | public class OmInfo { 12 | 13 | @Builder 14 | @Getter 15 | public static class Info { 16 | 17 | @JsonProperty("version") 18 | private String version; 19 | 20 | @JsonCreator 21 | public Info(@JsonProperty("version") String version) { 22 | this.version = version; 23 | } 24 | } 25 | 26 | @JsonProperty("info") 27 | private Info info; 28 | 29 | @JsonCreator 30 | public OmInfo(@JsonProperty("info") Info info) { 31 | this.info = info; 32 | } 33 | 34 | public Integer getMajorVersion() { 35 | String[] versionParts = info.getVersion().split("v"); 36 | String[] buildParts = versionParts[0].split("-"); 37 | String[] majorMinorParts = buildParts[0].split("\\."); 38 | return Integer.valueOf(majorMinorParts[0]); 39 | } 40 | 41 | public Integer getMinorVersion() { 42 | String[] versionParts = info.getVersion().split("v"); 43 | String[] buildParts = versionParts[0].split("-"); 44 | String[] majorMinorParts = buildParts[0].split("\\."); 45 | return Integer.valueOf(majorMinorParts[1]); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/notifier/AppRelationshipConsoleNotifier.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.notifier; 2 | 3 | import org.cftoolsuite.cfapp.config.PasSettings; 4 | import org.cftoolsuite.cfapp.event.AppRelationshipRetrievedEvent; 5 | import org.cftoolsuite.cfapp.report.AppRelationshipCsvReport; 6 | import org.cftoolsuite.cfapp.service.TimeKeeperService; 7 | import org.cftoolsuite.cfapp.service.TkServiceUtil; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.context.ApplicationListener; 10 | import org.springframework.stereotype.Component; 11 | 12 | import lombok.extern.slf4j.Slf4j; 13 | 14 | @Slf4j 15 | @Component 16 | public class AppRelationshipConsoleNotifier implements ApplicationListener { 17 | 18 | private final AppRelationshipCsvReport report; 19 | private final TkServiceUtil util; 20 | 21 | @Autowired 22 | public AppRelationshipConsoleNotifier( 23 | PasSettings appSettings, 24 | TimeKeeperService tkService) { 25 | this.report = new AppRelationshipCsvReport(appSettings); 26 | this.util = new TkServiceUtil(tkService); 27 | } 28 | 29 | @Override 30 | public void onApplicationEvent(AppRelationshipRetrievedEvent event) { 31 | util 32 | .getTimeCollected() 33 | .subscribe(tc -> log.trace(String.join("%n%n", report.generatePreamble(tc), report.generateDetail(event)))); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/OwnerNotificationTemplate.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | 5 | import com.fasterxml.jackson.annotation.JsonCreator; 6 | import com.fasterxml.jackson.annotation.JsonIgnore; 7 | import com.fasterxml.jackson.annotation.JsonInclude; 8 | import com.fasterxml.jackson.annotation.JsonProperty; 9 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 10 | 11 | import lombok.Builder; 12 | import lombok.Getter; 13 | 14 | @Builder 15 | @JsonInclude(JsonInclude.Include.NON_NULL) 16 | @JsonPropertyOrder({ "from", "subject", "body" }) 17 | @Getter 18 | public class OwnerNotificationTemplate { 19 | 20 | @JsonProperty("from") 21 | private String from; 22 | 23 | @JsonProperty("subject") 24 | private String subject; 25 | 26 | @JsonProperty("body") 27 | private String body; 28 | 29 | @JsonCreator 30 | public OwnerNotificationTemplate( 31 | @JsonProperty("from") String from, 32 | @JsonProperty("subject") String subject, 33 | @JsonProperty("body") String body 34 | ) { 35 | this.from = from; 36 | this.subject = subject; 37 | this.body = body; 38 | } 39 | 40 | @JsonIgnore 41 | public boolean isValid() { 42 | return EmailValidator.isValid(from) 43 | && StringUtils.isNotBlank(subject) 44 | && StringUtils.isNotBlank(body); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/SpaceUsersWriteConverter.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain; 2 | 3 | import java.util.stream.Collectors; 4 | 5 | import org.apache.commons.lang3.StringUtils; 6 | import org.springframework.core.convert.converter.Converter; 7 | import org.springframework.data.convert.WritingConverter; 8 | import org.springframework.data.r2dbc.mapping.OutboundRow; 9 | import org.springframework.r2dbc.core.Parameter; 10 | import org.springframework.stereotype.Indexed; 11 | 12 | @Indexed 13 | @WritingConverter 14 | public class SpaceUsersWriteConverter implements Converter { 15 | 16 | @Override 17 | public OutboundRow convert(SpaceUsers source) { 18 | OutboundRow row = new OutboundRow(); 19 | row.put("organization", Parameter.fromOrEmpty(source.getOrganization(), String.class)); 20 | row.put("space", Parameter.fromOrEmpty(source.getSpace(), String.class)); 21 | row.put("auditors", Parameter.fromOrEmpty(source.getAuditors().stream().filter(StringUtils::isNotBlank).collect(Collectors.joining(",")), String.class)); 22 | row.put("developers", Parameter.fromOrEmpty(source.getDevelopers().stream().filter(StringUtils::isNotBlank).collect(Collectors.joining(",")), String.class)); 23 | row.put("managers", Parameter.fromOrEmpty(source.getManagers().stream().filter(StringUtils::isNotBlank).collect(Collectors.joining(",")), String.class)); 24 | return row; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/notifier/ProductsAndReleasesConsoleNotifier.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.notifier; 2 | 3 | import org.cftoolsuite.cfapp.event.ProductsAndReleasesRetrievedEvent; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.context.ApplicationListener; 6 | import org.springframework.stereotype.Component; 7 | 8 | import lombok.extern.slf4j.Slf4j; 9 | import tools.jackson.core.JacksonException; 10 | import tools.jackson.databind.ObjectMapper; 11 | 12 | @Slf4j 13 | @Component 14 | public class ProductsAndReleasesConsoleNotifier implements ApplicationListener { 15 | 16 | private final ObjectMapper mapper; 17 | 18 | @Autowired 19 | public ProductsAndReleasesConsoleNotifier(ObjectMapper mapper) { 20 | this.mapper = mapper; 21 | } 22 | 23 | @Override 24 | public void onApplicationEvent(ProductsAndReleasesRetrievedEvent event) { 25 | try { 26 | log.trace(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(event.getProducts())); 27 | log.trace(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(event.getAllReleases())); 28 | log.trace(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(event.getLatestReleases())); 29 | } catch (JacksonException jpe) { 30 | log.error("Could not list products from Pivotal Network.", jpe); 31 | } 32 | } 33 | 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/service/R2dbcServiceInstanceMetricsService.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.service; 2 | 3 | import org.cftoolsuite.cfapp.repository.R2dbcServiceInstanceMetricsRepository; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.stereotype.Service; 6 | 7 | import reactor.core.publisher.Flux; 8 | import reactor.core.publisher.Mono; 9 | import reactor.util.function.Tuple2; 10 | 11 | @Service 12 | public class R2dbcServiceInstanceMetricsService implements ServiceInstanceMetricsService { 13 | 14 | private final R2dbcServiceInstanceMetricsRepository repo; 15 | 16 | @Autowired 17 | public R2dbcServiceInstanceMetricsService(R2dbcServiceInstanceMetricsRepository repo) { 18 | this.repo = repo; 19 | } 20 | 21 | @Override 22 | public Flux> byOrganization() { 23 | return repo.byOrganization(); 24 | } 25 | 26 | @Override 27 | public Flux> byService() { 28 | return repo.byService(); 29 | } 30 | 31 | @Override 32 | public Flux> byServiceAndPlan() { 33 | return repo.byServiceAndPlan(); 34 | } 35 | 36 | @Override 37 | public Mono totalServiceInstances() { 38 | return repo.totalServiceInstances(); 39 | } 40 | 41 | @Override 42 | public Flux> totalVelocity() { 43 | return repo.totalVelocity(); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/AppRelationshipRequest.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import lombok.Builder; 7 | import lombok.Getter; 8 | 9 | @Builder 10 | @Getter 11 | public class AppRelationshipRequest { 12 | 13 | public static List listOf(ServiceInstanceDetail detail) { 14 | List result = new ArrayList<>(); 15 | detail.getApplications().forEach(a -> { 16 | result.add(AppRelationshipRequest 17 | .builder() 18 | .organization(detail.getOrganization()) 19 | .space(detail.getSpace()) 20 | .applicationName(a) 21 | .serviceInstanceId(detail.getServiceInstanceId()) 22 | .serviceName(detail.getName()) 23 | .serviceOffering(detail.getService()) 24 | .serviceType(detail.getType()) 25 | .servicePlan(detail.getPlan()) 26 | .build()); 27 | }); 28 | return result; 29 | } 30 | private String organization; 31 | private String space; 32 | private String serviceInstanceId; 33 | private String serviceName; 34 | private String serviceOffering; 35 | private String applicationId; 36 | private String applicationName; 37 | private String serviceType; 38 | 39 | private String servicePlan; 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/UserCounts.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 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({ "by-organization", "total-user-accounts", "total-service-accounts"}) 19 | public class UserCounts { 20 | 21 | @Default 22 | @JsonProperty("by-organization") 23 | private Map byOrganization = new HashMap<>(); 24 | 25 | @Default 26 | @JsonProperty("total-user-accounts") 27 | private Long totalUserAccounts = 0L; 28 | 29 | @Default 30 | @JsonProperty("total-service-accounts") 31 | private Long totalServiceAccounts = 0L; 32 | 33 | @JsonCreator 34 | public UserCounts( 35 | @JsonProperty("by-organization") Map byOrganization, 36 | @JsonProperty("total-user-accounts") Long totalUserAccounts, 37 | @JsonProperty("total-service-accounts") Long totalServiceAccounts 38 | ) { 39 | this.byOrganization = byOrganization; 40 | this.totalUserAccounts = totalUserAccounts; 41 | this.totalServiceAccounts = totalServiceAccounts; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/repository/R2dbcSpaceRepository.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.repository; 2 | 3 | import org.cftoolsuite.cfapp.domain.Space; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.data.domain.Sort; 6 | import org.springframework.data.domain.Sort.Order; 7 | import org.springframework.data.r2dbc.core.R2dbcEntityOperations; 8 | import org.springframework.data.relational.core.query.Query; 9 | import org.springframework.stereotype.Repository; 10 | 11 | import reactor.core.publisher.Flux; 12 | import reactor.core.publisher.Mono; 13 | 14 | @Repository 15 | public class R2dbcSpaceRepository { 16 | 17 | private final R2dbcEntityOperations client; 18 | 19 | @Autowired 20 | public R2dbcSpaceRepository(R2dbcEntityOperations client) { 21 | this.client = client; 22 | } 23 | 24 | public Mono deleteAll() { 25 | return 26 | client 27 | .delete(Space.class) 28 | .all() 29 | .then(); 30 | } 31 | 32 | public Flux findAll() { 33 | return 34 | client 35 | .select(Space.class) 36 | .matching(Query.empty().sort(Sort.by(Order.asc("org_name"), Order.asc("space_name")))) 37 | .all(); 38 | } 39 | 40 | public Mono save(Space entity) { 41 | return 42 | client 43 | .insert(entity); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /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.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({"year", "total_task_runs", "maximum_concurrent_tasks", "task_hours"}) 14 | public class TaskUsageYearly { 15 | 16 | @JsonProperty("year") 17 | private Integer year; 18 | 19 | @Default 20 | @JsonProperty("total_task_runs") 21 | private Integer totalTaskRuns = 0; 22 | 23 | @Default 24 | @JsonProperty("maximum_concurrent_tasks") 25 | private Integer maximumConcurrentTasks = 0; 26 | 27 | @Default 28 | @JsonProperty("task_hours") 29 | private Double taskHours = 0.0; 30 | 31 | @JsonCreator 32 | public TaskUsageYearly( 33 | @JsonProperty("year") Integer year, 34 | @JsonProperty("total_task_runs") Integer totalTaskRuns, 35 | @JsonProperty("maximum_concurrent_tasks") Integer maximumConcurrentTasks, 36 | @JsonProperty("task_hours") Double taskHours) { 37 | this.year = year; 38 | this.totalTaskRuns = totalTaskRuns; 39 | this.maximumConcurrentTasks = maximumConcurrentTasks; 40 | this.taskHours = taskHours; 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/Defaults.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import org.cftoolsuite.cfapp.util.CsvUtil; 7 | 8 | import io.r2dbc.spi.Row; 9 | 10 | public class Defaults { 11 | 12 | public static List getColumnListOfStringValue(Row row, String columnName) { 13 | String csv = Defaults.getColumnValue(row, columnName, String.class); 14 | return new ArrayList<>(CsvUtil.parse(csv)); 15 | } 16 | 17 | public static T getColumnValue(Row row, String columnName, Class columnType) { 18 | try { 19 | return row.get(columnName, columnType); 20 | } catch (ClassCastException cce) { 21 | return null; 22 | } 23 | } 24 | 25 | public static T getColumnValueOrDefault(Row row, String columnName, Class columnType, T defaultValue) { 26 | try { 27 | T value = row.get(columnName, columnType); 28 | return value == null ? defaultValue : value; 29 | } catch (ClassCastException cce) { 30 | return defaultValue; 31 | } 32 | } 33 | 34 | public static Object getColumnValueOrDefault(Row row, String columnName, Object defaultValue) { 35 | try { 36 | Object value = row.get(columnName); 37 | return value == null ? defaultValue : value; 38 | } catch (Exception cce) { 39 | return defaultValue; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/test/java/org/cftoolsuite/cfapp/service/ServiceInstanceReporterTest.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.service; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | 6 | import org.assertj.core.api.Assertions; 7 | import org.cftoolsuite.cfapp.ButlerTest; 8 | import org.junit.jupiter.api.Test; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | 11 | import tools.jackson.core.exc.StreamReadException; 12 | import tools.jackson.databind.DatabindException; 13 | import tools.jackson.databind.ObjectMapper; 14 | 15 | 16 | @ButlerTest 17 | public class ServiceInstanceReporterTest { 18 | 19 | private final ServiceInstanceReporter reporter; 20 | private final ObjectMapper mapper; 21 | 22 | @Autowired 23 | public ServiceInstanceReporterTest( 24 | ServiceInstanceReporter reporter, 25 | ObjectMapper mapper 26 | ) { 27 | this.reporter = reporter; 28 | this.mapper = mapper; 29 | } 30 | 31 | @Test 32 | public void testReportGeneration() throws StreamReadException, DatabindException, IOException { 33 | File file = new File(System.getProperty("user.home") + "/service-instance-reporting-config.json"); 34 | if (file.exists()) { 35 | ReportRequestSpec spec = mapper.readValue(file, ReportRequestSpec.class); 36 | reporter.createReport(spec.getOutput(), spec.getInput()); 37 | Assertions.assertThat(new File(spec.getOutput()).exists()).isTrue(); 38 | } 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /docs/BUILD.md: -------------------------------------------------------------------------------- 1 | # VMware Tanzu Application Service > Butler 2 | 3 | ## How to Build 4 | 5 | ``` 6 | ./mvnw clean package 7 | ``` 8 | > Defaults to H2 in-memory backend 9 | 10 | ### Alternatives 11 | 12 | The below represent a collection of Maven profiles available in the Maven POM. 13 | 14 | * MySQL (mysql) 15 | * adds a dependency on [r2dbc-mysql](https://github.com/asyncer-io/r2dbc-mysql) 16 | * Postgres (postgres) 17 | * adds a dependency on [r2dbc-postrgesql](https://github.com/pgjdbc/r2dbc-postgresql) 18 | * Log4J2 logging (log4j2) 19 | * 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) 20 | * Native image (native) 21 | * 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/) 22 | 23 | 24 | ``` 25 | ./mvnw clean package -Drdbms=mysql 26 | ``` 27 | > Work with MySQL backend 28 | 29 | ``` 30 | ./mvnw clean package -Drdbms=postgres 31 | ``` 32 | > Work with Postgres backend 33 | 34 | ``` 35 | ./mvnw clean package -Plog4j2 36 | ``` 37 | > Swap out default "lossy" logging provider 38 | 39 | 40 | ``` 41 | # Using Cloud Native Buildpacks image 42 | ./mvnw spring-boot:build-image -Pnative 43 | 44 | # Using pre-installed Graal CE 45 | ./mvnw native:compile -Pnative -DskipTests 46 | ``` 47 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/config/GitSettings.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.config; 2 | 3 | import java.util.Set; 4 | 5 | import org.apache.commons.lang3.StringUtils; 6 | import org.springframework.boot.context.properties.ConfigurationProperties; 7 | import org.springframework.boot.context.properties.bind.ConstructorBinding; 8 | 9 | import lombok.Builder; 10 | import lombok.Builder.Default; 11 | import lombok.Getter; 12 | 13 | @Builder 14 | @Getter 15 | @ConfigurationProperties(prefix = "cf.policies.git") 16 | public class GitSettings { 17 | 18 | @Default 19 | private String uri = ""; 20 | private String username; 21 | @Default 22 | private String password = ""; 23 | private String commit; 24 | private Set filePaths; 25 | 26 | public boolean isAuthenticated() { 27 | return StringUtils.isNotBlank(getUsername()); 28 | } 29 | 30 | public boolean isPinnedCommit() { 31 | return StringUtils.isNotBlank(getCommit()); 32 | } 33 | 34 | public boolean isVersionManaged() { 35 | return StringUtils.isNotBlank(uri); 36 | } 37 | 38 | @ConstructorBinding 39 | GitSettings( 40 | String uri, 41 | String username, 42 | String password, 43 | String commit, 44 | Set filePaths) { 45 | this.uri = uri; 46 | this.username = username; 47 | this.password = password; 48 | this.commit = commit; 49 | this.filePaths = filePaths; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/event/ProductsAndReleasesRetrievedEvent.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.event; 2 | 3 | import java.util.List; 4 | 5 | import org.cftoolsuite.cfapp.domain.product.Products; 6 | import org.cftoolsuite.cfapp.domain.product.Release; 7 | import org.springframework.context.ApplicationEvent; 8 | 9 | public class ProductsAndReleasesRetrievedEvent extends ApplicationEvent { 10 | 11 | private static final long serialVersionUID = 1L; 12 | 13 | private Products products; 14 | private List allReleases; 15 | private List latestReleases; 16 | 17 | public ProductsAndReleasesRetrievedEvent(Object source) { 18 | super(source); 19 | } 20 | 21 | public ProductsAndReleasesRetrievedEvent allReleases(List allReleases) { 22 | this.allReleases = allReleases; 23 | return this; 24 | } 25 | 26 | public List getAllReleases() { 27 | return allReleases; 28 | } 29 | 30 | public List getLatestReleases() { 31 | return latestReleases; 32 | } 33 | 34 | public Products getProducts() { 35 | return products; 36 | } 37 | 38 | public ProductsAndReleasesRetrievedEvent latestReleases(List latestReleases) { 39 | this.latestReleases = latestReleases; 40 | return this; 41 | } 42 | 43 | public ProductsAndReleasesRetrievedEvent products(Products products) { 44 | this.products = products; 45 | return this; 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/Demographics.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({ "total-organizations", "total-spaces", "total-user-accounts", "total-service-accounts" }) 14 | public class Demographics { 15 | 16 | @Default 17 | @JsonProperty("total-organizations") 18 | private Long organizations = 0L; 19 | 20 | @Default 21 | @JsonProperty("total-spaces") 22 | private Long spaces = 0L; 23 | 24 | @Default 25 | @JsonProperty("total-user-accounts") 26 | private Long userAccounts = 0L; 27 | 28 | @Default 29 | @JsonProperty("total-service-accounts") 30 | private Long serviceAccounts = 0L; 31 | 32 | @JsonCreator 33 | public Demographics( 34 | @JsonProperty("total-organizations") Long organizations, 35 | @JsonProperty("total-spaces") Long spaces, 36 | @JsonProperty("total-user-accounts") Long userAccounts, 37 | @JsonProperty("total-service-accounts") Long serviceAccounts 38 | ) { 39 | this.organizations = organizations; 40 | this.spaces = spaces; 41 | this.userAccounts = userAccounts; 42 | this.serviceAccounts = serviceAccounts; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/notifier/ServiceInstanceDetailConsoleNotifier.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.notifier; 2 | 3 | import org.cftoolsuite.cfapp.config.PasSettings; 4 | import org.cftoolsuite.cfapp.event.ServiceInstanceDetailRetrievedEvent; 5 | import org.cftoolsuite.cfapp.report.ServiceInstanceDetailCsvReport; 6 | import org.cftoolsuite.cfapp.service.TimeKeeperService; 7 | import org.cftoolsuite.cfapp.service.TkServiceUtil; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.context.ApplicationListener; 10 | import org.springframework.stereotype.Component; 11 | 12 | import lombok.extern.slf4j.Slf4j; 13 | 14 | @Slf4j 15 | @Component 16 | public class ServiceInstanceDetailConsoleNotifier implements ApplicationListener { 17 | 18 | private final ServiceInstanceDetailCsvReport report; 19 | private final TkServiceUtil util; 20 | 21 | @Autowired 22 | public ServiceInstanceDetailConsoleNotifier( 23 | PasSettings appSettings, 24 | TimeKeeperService tkService) { 25 | this.report = new ServiceInstanceDetailCsvReport(appSettings); 26 | this.util = new TkServiceUtil(tkService); 27 | } 28 | 29 | @Override 30 | public void onApplicationEvent(ServiceInstanceDetailRetrievedEvent event) { 31 | util 32 | .getTimeCollected() 33 | .subscribe(tc -> log.trace(String.join("%n%n", report.generatePreamble(tc), report.generateDetail(event)))); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/service/BuildpacksCache.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.service; 2 | 3 | import java.util.HashMap; 4 | import java.util.List; 5 | import java.util.Map; 6 | 7 | import org.apache.commons.lang3.StringUtils; 8 | import org.cftoolsuite.cfapp.domain.Buildpack; 9 | import org.springframework.stereotype.Component; 10 | 11 | @Component 12 | public class BuildpacksCache { 13 | 14 | private final Map buildpacksById = new HashMap<>(); 15 | 16 | public Map from(List input) { 17 | buildpacksById.clear(); 18 | input.forEach( 19 | b -> { 20 | Buildpack buildpack = 21 | Buildpack 22 | .builder() 23 | .id(b.getId()) 24 | .name(b.getName()) 25 | .position(b.getPosition()) 26 | .enabled(b.getEnabled()) 27 | .locked(b.getLocked()) 28 | .filename(b.getFilename()) 29 | .build(); 30 | buildpacksById.put(b.getId(), buildpack); 31 | }); 32 | return buildpacksById; 33 | } 34 | 35 | public Buildpack getBuildpackById(String id) { 36 | if (StringUtils.isBlank(id)) { 37 | return null; 38 | } 39 | return buildpacksById.get(id); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/repository/R2dbcOrganizationRepository.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.repository; 2 | 3 | import org.cftoolsuite.cfapp.domain.Organization; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.data.domain.Sort; 6 | import org.springframework.data.domain.Sort.Order; 7 | import org.springframework.data.r2dbc.core.R2dbcEntityOperations; 8 | import org.springframework.data.relational.core.query.Query; 9 | import org.springframework.stereotype.Repository; 10 | 11 | import reactor.core.publisher.Flux; 12 | import reactor.core.publisher.Mono; 13 | 14 | @Repository 15 | public class R2dbcOrganizationRepository { 16 | 17 | private final R2dbcEntityOperations client; 18 | 19 | @Autowired 20 | public R2dbcOrganizationRepository(R2dbcEntityOperations client) { 21 | this.client = client; 22 | } 23 | 24 | public Mono deleteAll() { 25 | return 26 | client 27 | .delete(Organization.class) 28 | .all() 29 | .then(); 30 | } 31 | 32 | public Flux findAll() { 33 | return 34 | client 35 | .select(Organization.class) 36 | .matching(Query.empty().sort(Sort.by(Order.asc("org_name")))) 37 | .all(); 38 | } 39 | 40 | public Mono save(Organization entity) { 41 | return 42 | client 43 | .insert(entity); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/util/PolicyFilter.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.util; 2 | 3 | import java.util.HashSet; 4 | import java.util.Set; 5 | 6 | import org.cftoolsuite.cfapp.config.PasSettings; 7 | import org.cftoolsuite.cfapp.domain.HasOrganizationWhiteList; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.stereotype.Component; 10 | import org.springframework.util.CollectionUtils; 11 | 12 | @Component 13 | public class PolicyFilter { 14 | 15 | private PasSettings settings; 16 | 17 | @Autowired 18 | public PolicyFilter(PasSettings settings) { 19 | this.settings = settings; 20 | } 21 | 22 | public boolean isBlacklisted(String organization, String space) { 23 | if (settings.hasSpaceBlackList()) { 24 | return !settings.getSpaceBlackList().contains(String.join(":", organization, space)); 25 | } else { 26 | return !settings.getOrganizationBlackList().contains(organization); 27 | } 28 | } 29 | 30 | public boolean isWhitelisted(HasOrganizationWhiteList policy, String organization) { 31 | Set prunedSet = new HashSet<>(policy.getOrganizationWhiteList()); 32 | while (prunedSet.remove("")); 33 | Set whitelist = 34 | CollectionUtils.isEmpty(prunedSet) ? 35 | prunedSet: policy.getOrganizationWhiteList(); 36 | return 37 | whitelist.isEmpty() ? true: policy.getOrganizationWhiteList().contains(organization); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/controller/DemographicsController.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.controller; 2 | 3 | import org.cftoolsuite.cfapp.domain.Demographics; 4 | import org.cftoolsuite.cfapp.service.DemographicsService; 5 | import org.cftoolsuite.cfapp.service.TimeKeeperService; 6 | import org.cftoolsuite.cfapp.service.TkServiceUtil; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.http.HttpStatus; 9 | import org.springframework.http.ResponseEntity; 10 | import org.springframework.web.bind.annotation.GetMapping; 11 | import org.springframework.web.bind.annotation.RestController; 12 | 13 | import reactor.core.publisher.Mono; 14 | 15 | @RestController 16 | public class DemographicsController { 17 | 18 | private final DemographicsService demoService; 19 | private final TkServiceUtil util; 20 | 21 | @Autowired 22 | public DemographicsController( 23 | DemographicsService demoService, 24 | TimeKeeperService tkService 25 | ) { 26 | this.demoService = demoService; 27 | this.util = new TkServiceUtil(tkService); 28 | } 29 | 30 | @GetMapping("/snapshot/demographics") 31 | public Mono> getDemographics() { 32 | return util.getHeaders() 33 | .flatMap(h -> demoService 34 | .getDemographics() 35 | .map(d -> new ResponseEntity<>(d, h, HttpStatus.OK))) 36 | .defaultIfEmpty(ResponseEntity.notFound().build()); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/UserSpaces.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.stream.Collectors; 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.Builder; 12 | import lombok.Builder.Default; 13 | import lombok.Getter; 14 | 15 | @Builder 16 | @Getter 17 | @JsonPropertyOrder({ "account-name", "spaces" }) 18 | public class UserSpaces { 19 | 20 | @JsonProperty("account-name") 21 | private String accountName; 22 | 23 | @Default 24 | @JsonProperty("spaces") 25 | private List spaces = new ArrayList<>(); 26 | 27 | 28 | @JsonCreator 29 | public UserSpaces( 30 | @JsonProperty("account-name") String accountName, 31 | @JsonProperty("spaces") List spaces) 32 | { 33 | this.accountName = accountName; 34 | this.spaces = spaces; 35 | } 36 | 37 | @Override 38 | public String toString() { 39 | return String.format( 40 | "User: %s, Spaces: [%s]", 41 | getAccountName(), 42 | String.join(",", getSpaces() 43 | .stream() 44 | .map(s -> 45 | String.join("/", s.getOrganizationName(), s.getSpaceName()) 46 | ) 47 | .collect(Collectors.toList()))); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/accounting/service/ServiceUsageReport.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.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({"report_time", "monthly_service_reports", "yearly_service_report"}) 17 | public class ServiceUsageReport { 18 | 19 | @JsonProperty("report_time") 20 | public String reportTime; 21 | 22 | @Default 23 | @JsonProperty("monthly_service_reports") 24 | public List monthlyServiceReports = new ArrayList<>(); 25 | 26 | @Default 27 | @JsonProperty("yearly_service_report") 28 | public List yearlyServiceReport = new ArrayList<>(); 29 | 30 | @JsonCreator 31 | public ServiceUsageReport( 32 | @JsonProperty("report_time") String reportTime, 33 | @JsonProperty("monthly_service_reports") List monthlyServiceReports, 34 | @JsonProperty("yearly_service_report") List yearlyServiceReport) { 35 | this.reportTime = reportTime; 36 | this.monthlyServiceReports = monthlyServiceReports; 37 | this.yearlyServiceReport = yearlyServiceReport; 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/service/R2dbcHistoricalRecordService.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.service; 2 | 3 | import java.time.LocalDate; 4 | 5 | import org.cftoolsuite.cfapp.domain.HistoricalRecord; 6 | import org.cftoolsuite.cfapp.repository.R2dbcHistoricalRecordRepository; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.stereotype.Service; 9 | import org.springframework.transaction.annotation.Transactional; 10 | 11 | import lombok.extern.slf4j.Slf4j; 12 | import reactor.core.publisher.Flux; 13 | import reactor.core.publisher.Mono; 14 | 15 | @Slf4j 16 | @Service 17 | public class R2dbcHistoricalRecordService implements HistoricalRecordService { 18 | 19 | private final R2dbcHistoricalRecordRepository repo; 20 | 21 | @Autowired 22 | public R2dbcHistoricalRecordService(R2dbcHistoricalRecordRepository repo) { 23 | this.repo = repo; 24 | } 25 | 26 | @Override 27 | public Flux findAll() { 28 | return repo.findAll(); 29 | } 30 | 31 | @Override 32 | public Flux findByDateRange(LocalDate start, LocalDate end) { 33 | return repo.findByDateRange(start, end); 34 | } 35 | 36 | @Override 37 | @Transactional 38 | public Mono save(HistoricalRecord entity) { 39 | return repo 40 | .save(entity) 41 | .onErrorContinue( 42 | (ex, data) -> log.error(String.format("Problem saving historical record %s.", entity), ex)); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /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.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({"year", "average_app_instances", "maximum_app_instances", "app_instance_hours"}) 14 | public class AppUsageYearly { 15 | 16 | @JsonProperty("year") 17 | private Integer year; 18 | 19 | @Default 20 | @JsonProperty("average_app_instances") 21 | private Double averageAppInstances = 0.0; 22 | 23 | @Default 24 | @JsonProperty("maximum_app_instances") 25 | private Integer maximumAppInstances = 0; 26 | 27 | @Default 28 | @JsonProperty("app_instance_hours") 29 | private Double appInstanceHours = 0.0; 30 | 31 | @JsonCreator 32 | public AppUsageYearly( 33 | @JsonProperty("year") Integer year, 34 | @JsonProperty("average_app_instances") Double averageAppInstances, 35 | @JsonProperty("maximum_app_instances") Integer maximumAppInstances, 36 | @JsonProperty("app_instance_hours") Double appInstanceHours) { 37 | this.year = year; 38 | this.averageAppInstances = averageAppInstances; 39 | this.maximumAppInstances = maximumAppInstances; 40 | this.appInstanceHours = appInstanceHours; 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /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.List; 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.Builder; 12 | import lombok.Builder.Default; 13 | import lombok.Getter; 14 | 15 | @Builder 16 | @Getter 17 | @JsonPropertyOrder({"service_name", "service_guid", "usages", "plans"}) 18 | public class ServiceUsageMonthlyAggregate { 19 | 20 | @JsonProperty("service_name") 21 | public String serviceName; 22 | 23 | @JsonProperty("service_guid") 24 | public String serviceGuid; 25 | 26 | @Default 27 | @JsonProperty("usages") 28 | public List usages = new ArrayList<>(); 29 | 30 | @Default 31 | @JsonProperty("plans") 32 | public List plans = new ArrayList<>(); 33 | 34 | @JsonCreator 35 | public ServiceUsageMonthlyAggregate( 36 | @JsonProperty("service_name") String serviceName, 37 | @JsonProperty("service_guid") String serviceGuid, 38 | @JsonProperty("usages") List usages, 39 | @JsonProperty("plans") List plans) { 40 | this.serviceName = serviceName; 41 | this.serviceGuid = serviceGuid; 42 | this.usages = usages; 43 | this.plans = plans; 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/test/java/org/cftoolsuite/cfapp/util/JsonToCsvConverterTest.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.util; 2 | 3 | import static org.junit.jupiter.api.Assertions.fail; 4 | 5 | import org.cftoolsuite.cfapp.ButlerTest; 6 | import org.junit.jupiter.api.Assertions; 7 | import org.junit.jupiter.api.Test; 8 | import org.junit.jupiter.api.extension.ExtendWith; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.test.context.junit.jupiter.SpringExtension; 11 | 12 | import tools.jackson.core.JacksonException; 13 | 14 | @ButlerTest 15 | @ExtendWith(SpringExtension.class) 16 | public class JsonToCsvConverterTest { 17 | 18 | @Autowired 19 | private JsonToCsvConverter converter; 20 | 21 | @Test 22 | public void testValidJson() { 23 | try { 24 | converter.convert("{ \"foo\": \"bar\" }"); 25 | } catch (JacksonException e) { 26 | fail(); 27 | } 28 | } 29 | 30 | @Test 31 | public void testValidJsonArray() { 32 | try { 33 | converter.convert("[{ \"foo\": \"bar\" }]"); 34 | } catch (JacksonException e) { 35 | fail(); 36 | } 37 | } 38 | 39 | @Test 40 | public void testStringIsEmpty() { 41 | Assertions.assertThrows(IllegalArgumentException.class, () -> { 42 | converter.convert("[]"); 43 | }); 44 | } 45 | 46 | @Test 47 | public void testInvalidInput() { 48 | Assertions.assertThrows(JacksonException.class, () -> { 49 | converter.convert("[{ \"foo\": \"bar\" }"); 50 | }); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/report/AppDetailCsvReport.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.report; 2 | 3 | import java.time.LocalDateTime; 4 | 5 | import org.cftoolsuite.cfapp.config.PasSettings; 6 | import org.cftoolsuite.cfapp.domain.AppDetail; 7 | import org.cftoolsuite.cfapp.event.AppDetailRetrievedEvent; 8 | 9 | public class AppDetailCsvReport { 10 | 11 | private PasSettings appSettings; 12 | 13 | public AppDetailCsvReport(PasSettings appSettings) { 14 | this.appSettings = appSettings; 15 | } 16 | 17 | public String generateDetail(AppDetailRetrievedEvent event) { 18 | StringBuffer details = new StringBuffer(); 19 | details.append("\n"); 20 | details.append(AppDetail.headers()); 21 | details.append("\n"); 22 | event.getDetail() 23 | .forEach(a -> { 24 | details.append(a.toCsv()); 25 | details.append("\n"); 26 | }); 27 | return details.toString(); 28 | } 29 | 30 | public String generatePreamble(LocalDateTime collectionTime) { 31 | StringBuffer preamble = new StringBuffer(); 32 | preamble.append("Application inventory detail from "); 33 | preamble.append(appSettings.getApiHost()); 34 | if (collectionTime != null) { 35 | preamble.append(" collected "); 36 | preamble.append(collectionTime); 37 | preamble.append(" and"); 38 | } 39 | preamble.append(" generated "); 40 | preamble.append(LocalDateTime.now()); 41 | preamble.append("."); 42 | return preamble.toString(); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/report/UserAccountsCsvReport.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.report; 2 | 3 | import java.time.LocalDateTime; 4 | 5 | import org.cftoolsuite.cfapp.config.PasSettings; 6 | import org.cftoolsuite.cfapp.domain.UserAccounts; 7 | import org.cftoolsuite.cfapp.event.UserAccountsRetrievedEvent; 8 | 9 | public class UserAccountsCsvReport { 10 | 11 | private PasSettings appSettings; 12 | 13 | public UserAccountsCsvReport(PasSettings appSettings) { 14 | this.appSettings = appSettings; 15 | } 16 | 17 | public String generateDetail(UserAccountsRetrievedEvent event) { 18 | StringBuffer details = new StringBuffer(); 19 | details.append("\n"); 20 | details.append(UserAccounts.headers()); 21 | details.append("\n"); 22 | event.getDetail() 23 | .forEach(a -> { 24 | details.append(a.toCsv()); 25 | details.append("\n"); 26 | }); 27 | return details.toString(); 28 | } 29 | 30 | public String generatePreamble(LocalDateTime collectionTime) { 31 | StringBuffer preamble = new StringBuffer(); 32 | preamble.append("User accounts from "); 33 | preamble.append(appSettings.getApiHost()); 34 | if (collectionTime != null) { 35 | preamble.append(" collected "); 36 | preamble.append(collectionTime); 37 | preamble.append(" and"); 38 | } 39 | preamble.append(" generated "); 40 | preamble.append(LocalDateTime.now()); 41 | preamble.append("."); 42 | return preamble.toString(); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/service/StacksCache.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.service; 2 | 3 | import java.util.HashMap; 4 | import java.util.List; 5 | import java.util.Map; 6 | import java.util.TreeMap; 7 | 8 | import org.apache.commons.lang3.StringUtils; 9 | import org.cftoolsuite.cfapp.domain.Stack; 10 | import org.springframework.stereotype.Component; 11 | import org.springframework.util.Assert; 12 | 13 | @Component 14 | public class StacksCache { 15 | 16 | private final Map stacksByName = new TreeMap<>(); 17 | private final Map stacksById = new HashMap<>(); 18 | 19 | public boolean contains(String name) { 20 | return stacksByName.containsKey(name); 21 | } 22 | 23 | public Map from(List input) { 24 | stacksByName.clear(); 25 | stacksById.clear(); 26 | input.forEach( 27 | s -> { 28 | Stack stack = Stack.builder().id(s.getId()).name(s.getName()).description(s.getDescription()).build(); 29 | stacksByName.put(s.getName(), stack); 30 | stacksById.put(s.getId(), stack); 31 | }); 32 | return stacksByName; 33 | } 34 | 35 | public Stack getStackById(String id) { 36 | Assert.isTrue(StringUtils.isNotBlank(id), "Stack id must not be blank."); 37 | return stacksById.get(id); 38 | } 39 | 40 | public Stack getStackByName(String name) { 41 | Assert.isTrue(StringUtils.isNotBlank(name), "Stack name must not be blank."); 42 | return stacksByName.get(name); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/controller/UserSpacesController.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.controller; 2 | 3 | import org.cftoolsuite.cfapp.domain.UserSpaces; 4 | import org.cftoolsuite.cfapp.service.TimeKeeperService; 5 | import org.cftoolsuite.cfapp.service.TkServiceUtil; 6 | import org.cftoolsuite.cfapp.service.UserSpacesService; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.http.HttpStatus; 9 | import org.springframework.http.ResponseEntity; 10 | import org.springframework.web.bind.annotation.GetMapping; 11 | import org.springframework.web.bind.annotation.PathVariable; 12 | import org.springframework.web.bind.annotation.RestController; 13 | 14 | import reactor.core.publisher.Mono; 15 | 16 | @RestController 17 | public class UserSpacesController { 18 | 19 | private final UserSpacesService service; 20 | private final TkServiceUtil util; 21 | 22 | @Autowired 23 | public UserSpacesController( 24 | UserSpacesService service, 25 | TimeKeeperService tkService) { 26 | this.service = service; 27 | this.util = new TkServiceUtil(tkService); 28 | } 29 | 30 | @GetMapping(value = { "/snapshot/spaces/users/{name}" }) 31 | public Mono> getSpacesForAccountName(@PathVariable("name") String name) { 32 | return util.getHeaders() 33 | .flatMap(h -> service 34 | .getUserSpaces(name) 35 | .map(userSpaces -> new ResponseEntity<>(userSpaces, h, HttpStatus.OK))) 36 | .defaultIfEmpty(ResponseEntity.notFound().build()); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/report/AppRelationshipCsvReport.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.report; 2 | 3 | import java.time.LocalDateTime; 4 | 5 | import org.cftoolsuite.cfapp.config.PasSettings; 6 | import org.cftoolsuite.cfapp.domain.AppRelationship; 7 | import org.cftoolsuite.cfapp.event.AppRelationshipRetrievedEvent; 8 | 9 | public class AppRelationshipCsvReport { 10 | 11 | private PasSettings appSettings; 12 | 13 | public AppRelationshipCsvReport(PasSettings appSettings) { 14 | this.appSettings = appSettings; 15 | } 16 | 17 | public String generateDetail(AppRelationshipRetrievedEvent event) { 18 | StringBuffer details = new StringBuffer(); 19 | details.append("\n"); 20 | details.append(AppRelationship.headers()); 21 | details.append("\n"); 22 | event.getRelations() 23 | .forEach(a -> { 24 | details.append(a.toCsv()); 25 | details.append("\n"); 26 | }); 27 | return details.toString(); 28 | } 29 | 30 | public String generatePreamble(LocalDateTime collectionTime) { 31 | StringBuffer preamble = new StringBuffer(); 32 | preamble.append("Application relationships from "); 33 | preamble.append(appSettings.getApiHost()); 34 | if (collectionTime != null) { 35 | preamble.append(" collected "); 36 | preamble.append(collectionTime); 37 | preamble.append(" and"); 38 | } 39 | preamble.append(" generated "); 40 | preamble.append(LocalDateTime.now()); 41 | preamble.append("."); 42 | return preamble.toString(); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/test/java/org/cftoolsuite/cfapp/service/ApplicationReporterTest.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.service; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | 6 | import org.assertj.core.api.Assertions; 7 | import org.cftoolsuite.cfapp.ButlerTest; 8 | import org.junit.jupiter.api.Test; 9 | import org.junit.jupiter.api.extension.ExtendWith; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.test.context.junit.jupiter.SpringExtension; 12 | 13 | import tools.jackson.core.exc.StreamReadException; 14 | import tools.jackson.databind.DatabindException; 15 | import tools.jackson.databind.ObjectMapper; 16 | 17 | 18 | @ButlerTest 19 | @ExtendWith(SpringExtension.class) 20 | public class ApplicationReporterTest { 21 | 22 | private final ApplicationReporter reporter; 23 | private final ObjectMapper mapper; 24 | 25 | @Autowired 26 | public ApplicationReporterTest( 27 | ApplicationReporter reporter, 28 | ObjectMapper mapper 29 | ) { 30 | this.reporter = reporter; 31 | this.mapper = mapper; 32 | } 33 | 34 | @Test 35 | public void testReportGeneration() throws StreamReadException, DatabindException, IOException { 36 | File file = new File(System.getProperty("user.home") + "/app-reporting-config.json"); 37 | if (file.exists()) { 38 | ReportRequestSpec spec = mapper.readValue(file, ReportRequestSpec.class); 39 | reporter.createReport(spec.getOutput(), spec.getInput()); 40 | Assertions.assertThat(new File(spec.getOutput()).exists()).isTrue(); 41 | } 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /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 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /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.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({"month", "year", "total_task_runs", "maximum_concurrent_tasks", "task_hours"}) 14 | public class TaskUsageMonthly { 15 | 16 | @JsonProperty("month") 17 | private Integer month; 18 | 19 | @JsonProperty("year") 20 | private Integer year; 21 | 22 | @Default 23 | @JsonProperty("total_task_runs") 24 | private Integer totalTaskRuns = 0; 25 | 26 | @Default 27 | @JsonProperty("maximum_concurrent_tasks") 28 | private Integer maximumConcurrentTasks = 0; 29 | 30 | @Default 31 | @JsonProperty("task_hours") 32 | private Double taskHours = 0.0; 33 | 34 | @JsonCreator 35 | public TaskUsageMonthly( 36 | @JsonProperty("month") Integer month, 37 | @JsonProperty("year") Integer year, 38 | @JsonProperty("total_task_runs") Integer totalTaskRuns, 39 | @JsonProperty("maximum_concurrent_tasks") Integer maximumConcurrentTasks, 40 | @JsonProperty("task_hours") Double taskHours) { 41 | this.month = month; 42 | this.year = year; 43 | this.totalTaskRuns = totalTaskRuns; 44 | this.maximumConcurrentTasks = maximumConcurrentTasks; 45 | this.taskHours = taskHours; 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/report/ServiceInstanceDetailCsvReport.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.report; 2 | 3 | import java.time.LocalDateTime; 4 | 5 | import org.cftoolsuite.cfapp.config.PasSettings; 6 | import org.cftoolsuite.cfapp.domain.ServiceInstanceDetail; 7 | import org.cftoolsuite.cfapp.event.ServiceInstanceDetailRetrievedEvent; 8 | 9 | public class ServiceInstanceDetailCsvReport { 10 | 11 | private PasSettings settings; 12 | 13 | public ServiceInstanceDetailCsvReport(PasSettings settings) { 14 | this.settings = settings; 15 | } 16 | 17 | public String generateDetail(ServiceInstanceDetailRetrievedEvent event) { 18 | StringBuffer detail = new StringBuffer(); 19 | detail.append("\n"); 20 | detail.append(ServiceInstanceDetail.headers()); 21 | detail.append("\n"); 22 | event.getDetail() 23 | .forEach(a -> { 24 | detail.append(a.toCsv()); 25 | detail.append("\n"); 26 | }); 27 | return detail.toString(); 28 | } 29 | 30 | public String generatePreamble(LocalDateTime collectionTime) { 31 | StringBuffer preamble = new StringBuffer(); 32 | preamble.append("Service inventory detail from "); 33 | preamble.append(settings.getApiHost()); 34 | if (collectionTime != null) { 35 | preamble.append(" collected "); 36 | preamble.append(collectionTime); 37 | preamble.append(" and"); 38 | } 39 | preamble.append(" generated "); 40 | preamble.append(LocalDateTime.now()); 41 | preamble.append("."); 42 | return preamble.toString(); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /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.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({"month", "year", "duration_in_hours", "average_instances", "maximum_instances"}) 14 | public class ServiceUsageMonthly { 15 | 16 | @JsonProperty("month") 17 | public Integer month; 18 | 19 | @JsonProperty("year") 20 | public Integer year; 21 | 22 | @Default 23 | @JsonProperty("duration_in_hours") 24 | public Double durationInHours = 0.0; 25 | 26 | @Default 27 | @JsonProperty("average_instances") 28 | public Double averageInstances = 0.0; 29 | 30 | @Default 31 | @JsonProperty("maximum_instances") 32 | public Integer maximumInstances = 0; 33 | 34 | @JsonCreator 35 | public ServiceUsageMonthly( 36 | @JsonProperty("month") Integer month, 37 | @JsonProperty("year") Integer year, 38 | @JsonProperty("duration_in_hours") Double durationInHours, 39 | @JsonProperty("average_instances") Double averageInstances, 40 | @JsonProperty("maximum_instances") Integer maximumInstances) { 41 | this.month = month; 42 | this.year = year; 43 | this.durationInHours = durationInHours; 44 | this.averageInstances = averageInstances; 45 | this.maximumInstances = maximumInstances; 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/HistoricalRecord.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain; 2 | 3 | import java.time.LocalDateTime; 4 | 5 | import org.apache.commons.lang3.StringUtils; 6 | import org.springframework.data.annotation.Id; 7 | import org.springframework.data.relational.core.mapping.Table; 8 | 9 | import com.fasterxml.jackson.annotation.JsonIgnore; 10 | 11 | import lombok.Builder; 12 | import lombok.EqualsAndHashCode; 13 | import lombok.Getter; 14 | 15 | @Builder 16 | @Getter 17 | @EqualsAndHashCode 18 | @Table("historical_record") 19 | public class HistoricalRecord { 20 | 21 | public static String headers() { 22 | return String.join(",", "transaction date/time", "action taken", "organization", "space", 23 | "application id", "service instance id", "type", "name"); 24 | } 25 | private static String wrap(String value) { 26 | return value != null ? StringUtils.wrap(value, '"') : StringUtils.wrap("", '"'); 27 | } 28 | @Id 29 | @JsonIgnore 30 | private Long pk; 31 | private LocalDateTime transactionDateTime; 32 | private String actionTaken; 33 | private String organization; 34 | private String space; 35 | private String appId; 36 | private String serviceInstanceId; 37 | 38 | private String type; 39 | 40 | private String name; 41 | 42 | public String toCsv() { 43 | return String.join(",", 44 | wrap(getTransactionDateTime() != null ? getTransactionDateTime().toString() : ""), 45 | wrap(getActionTaken()), wrap(getOrganization()), wrap(getSpace()), wrap(getAppId()), 46 | wrap(getServiceInstanceId()), wrap(getType()), wrap(getName())); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/ServiceInstanceOperation.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain; 2 | 3 | import java.util.Arrays; 4 | import java.util.EnumMap; 5 | import java.util.Map; 6 | import java.util.stream.Collectors; 7 | 8 | import org.cftoolsuite.cfapp.task.DeleteServiceInstancePolicyExecutorTask; 9 | import org.cftoolsuite.cfapp.task.PolicyExecutorTask; 10 | import org.springframework.util.Assert; 11 | 12 | import com.fasterxml.jackson.annotation.JsonValue; 13 | 14 | public enum ServiceInstanceOperation { 15 | 16 | DELETE("delete"); 17 | 18 | private final String name; 19 | 20 | ServiceInstanceOperation(String name) { 21 | this.name = name; 22 | } 23 | 24 | static final Map> operationTaskMap = new EnumMap<>(ServiceInstanceOperation.class); 25 | static { 26 | operationTaskMap.put(ServiceInstanceOperation.DELETE, DeleteServiceInstancePolicyExecutorTask.class); 27 | } 28 | 29 | public static ServiceInstanceOperation from(String name) { 30 | Assert.hasText(name, "ServiceInstanceOperation must not be null or empty"); 31 | ServiceInstanceOperation result = Arrays.asList(ServiceInstanceOperation.values()).stream().filter(s -> s.getName().equalsIgnoreCase(name)).collect(Collectors.toList()).get(0); 32 | Assert.notNull(result, String.format("Invalid ServiceInstanceOperation, name=%s", name)); 33 | return result; 34 | } 35 | 36 | public static Class getTaskType(String op) { 37 | return operationTaskMap.get(from(op)); 38 | } 39 | 40 | @JsonValue 41 | public String getName() { 42 | return name; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/AppRelationship.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | import org.springframework.data.annotation.Id; 5 | import org.springframework.data.relational.core.mapping.Table; 6 | 7 | import com.fasterxml.jackson.annotation.JsonIgnore; 8 | 9 | import lombok.Builder; 10 | import lombok.EqualsAndHashCode; 11 | import lombok.Getter; 12 | import lombok.ToString; 13 | 14 | @Builder 15 | @Getter 16 | @EqualsAndHashCode 17 | @ToString 18 | @Table("application_relationship") 19 | public class AppRelationship { 20 | 21 | public static String headers() { 22 | return String.join(",", "organization", "space", "application id", 23 | "application name", "service instance id", "service name", "service offering", "service plan", "service type"); 24 | } 25 | private static String wrap(String value) { 26 | return value != null ? StringUtils.wrap(value, '"') : StringUtils.wrap("", '"'); 27 | } 28 | @Id 29 | @JsonIgnore 30 | private Long pk; 31 | private String organization; 32 | private String space; 33 | private String appId; 34 | private String appName; 35 | private String serviceInstanceId; 36 | private String serviceName; 37 | private String serviceOffering; 38 | 39 | private String servicePlan; 40 | 41 | private String serviceType; 42 | 43 | public String toCsv() { 44 | return String.join(",", wrap(getOrganization()), wrap(getSpace()), wrap(getAppId()), wrap(getAppName()), 45 | wrap(getServiceInstanceId()), wrap(getServiceName()), wrap(getServiceOffering()), wrap(getServicePlan()), wrap(getServiceType())); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /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.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({"month", "year", "average_app_instances","maximum_app_instances", "app_instance_hours"}) 14 | public class AppUsageMonthly { 15 | 16 | @JsonProperty("month") 17 | private Integer month; 18 | 19 | @JsonProperty("year") 20 | private Integer year; 21 | 22 | @Default 23 | @JsonProperty("average_app_instances") 24 | private Double averageAppInstances = 0.0; 25 | 26 | @Default 27 | @JsonProperty("maximum_app_instances") 28 | private Integer maximumAppInstances = 0; 29 | 30 | @Default 31 | @JsonProperty("app_instance_hours") 32 | private Double appInstanceHours = 0.0; 33 | 34 | @JsonCreator 35 | public AppUsageMonthly( 36 | @JsonProperty("month") Integer month, 37 | @JsonProperty("year") Integer year, 38 | @JsonProperty("average_app_instances") Double averageAppInstances, 39 | @JsonProperty("maximum_app_instances") Integer maximumAppInstances, 40 | @JsonProperty("app_instance_hours") Double appInstanceHours) { 41 | this.month = month; 42 | this.year = year; 43 | this.averageAppInstances = averageAppInstances; 44 | this.maximumAppInstances = maximumAppInstances; 45 | this.appInstanceHours = appInstanceHours; 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/test/java/org/cftoolsuite/cfapp/repository/R2dbcSpaceRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.repository; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import java.util.UUID; 6 | 7 | import org.cftoolsuite.cfapp.ButlerTest; 8 | import org.cftoolsuite.cfapp.domain.Space; 9 | import org.junit.jupiter.api.BeforeEach; 10 | import org.junit.jupiter.api.Test; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | 13 | import reactor.test.StepVerifier; 14 | 15 | @ButlerTest 16 | public class R2dbcSpaceRepositoryTest { 17 | 18 | private final R2dbcSpaceRepository repo; 19 | 20 | @Autowired 21 | public R2dbcSpaceRepositoryTest( 22 | R2dbcSpaceRepository repo 23 | ) { 24 | this.repo = repo; 25 | } 26 | 27 | @BeforeEach 28 | public void setup() { 29 | StepVerifier.create(repo.deleteAll()).verifyComplete(); 30 | } 31 | 32 | @Test 33 | public void testSaveWasSuccessful() { 34 | String organizationId = UUID.randomUUID().toString(); 35 | String spaceId = UUID.randomUUID().toString(); 36 | Space entity = Space 37 | .builder() 38 | .spaceId(spaceId) 39 | .spaceName("dev") 40 | .organizationId(organizationId) 41 | .organizationName("zoo-labs") 42 | .build(); 43 | StepVerifier.create(repo.save(entity) 44 | .thenMany(repo.findAll())) 45 | .assertNext(s -> { 46 | assertEquals(spaceId, s.getSpaceId()); 47 | assertEquals("dev", s.getSpaceName()); 48 | assertEquals(organizationId, s.getOrganizationId()); 49 | assertEquals("zoo-labs", s.getOrganizationName()); 50 | }).verifyComplete(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/controller/OnDemandPolicyTriggerController.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.controller; 2 | 3 | import org.cftoolsuite.cfapp.service.PoliciesService; 4 | import org.cftoolsuite.cfapp.task.PolicyExecutorTask; 5 | import org.springframework.beans.factory.BeanFactory; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.context.annotation.Profile; 8 | import org.springframework.http.ResponseEntity; 9 | import org.springframework.web.bind.annotation.PostMapping; 10 | import org.springframework.web.bind.annotation.RestController; 11 | 12 | import reactor.core.publisher.Flux; 13 | import reactor.core.publisher.Mono; 14 | 15 | @Profile("on-demand") 16 | @RestController 17 | public class OnDemandPolicyTriggerController { 18 | 19 | private BeanFactory factory; 20 | private PoliciesService service; 21 | 22 | @Autowired 23 | public OnDemandPolicyTriggerController(BeanFactory factory, PoliciesService service) { 24 | this.factory = factory; 25 | this.service = service; 26 | } 27 | 28 | @PostMapping("/policies/execute") 29 | public Mono> triggerPolicyExecution() { 30 | return service.getTaskMap() 31 | .flatMapMany(taskTypeMap -> 32 | Flux.fromIterable(taskTypeMap.entrySet()) 33 | .flatMap(entry -> { 34 | String policyId = entry.getKey(); 35 | Class taskClass = entry.getValue(); 36 | PolicyExecutorTask task = factory.getBean(taskClass); 37 | return Mono.fromRunnable(() -> task.execute(policyId)); 38 | })) 39 | .then(Mono.just(ResponseEntity.accepted().build())); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/Space.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain; 2 | 3 | import org.springframework.data.annotation.Id; 4 | import org.springframework.data.annotation.PersistenceCreator; 5 | import org.springframework.data.relational.core.mapping.Column; 6 | import org.springframework.data.relational.core.mapping.Table; 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.EqualsAndHashCode; 14 | import lombok.Getter; 15 | import lombok.ToString; 16 | 17 | @Builder 18 | @Getter 19 | @JsonPropertyOrder({ "organization-id", "organization-name", "space-id", "space-name" }) 20 | @EqualsAndHashCode 21 | @ToString 22 | @Table("spaces") 23 | public class Space { 24 | 25 | @Column("org_id") 26 | @JsonProperty("organization-id") 27 | private final String organizationId; 28 | 29 | @Column("org_name") 30 | @JsonProperty("organization-name") 31 | private final String organizationName; 32 | 33 | @Id 34 | @JsonProperty("space-id") 35 | private final String spaceId; 36 | 37 | @JsonProperty("space-name") 38 | private final String spaceName; 39 | 40 | 41 | @JsonCreator 42 | @PersistenceCreator 43 | Space( 44 | @JsonProperty("organization-id") String organizationId, 45 | @JsonProperty("organization-name") String organizationName, 46 | @JsonProperty("space-id") String spaceId, 47 | @JsonProperty("space-name") String spaceName) { 48 | this.organizationId = organizationId; 49 | this.organizationName = organizationName; 50 | this.spaceId = spaceId; 51 | this.spaceName = spaceName; 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/product/ReleaseLinks.java: -------------------------------------------------------------------------------- 1 | 2 | package org.cftoolsuite.cfapp.domain.product; 3 | 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 6 | 7 | import lombok.Builder; 8 | import lombok.Getter; 9 | 10 | 11 | @Builder 12 | @Getter 13 | @JsonPropertyOrder({ 14 | "self", 15 | "eula_acceptance", 16 | "product_files", 17 | "file_groups", 18 | "user_groups", 19 | "artifact_references" 20 | }) 21 | public class ReleaseLinks { 22 | 23 | @JsonProperty("self") 24 | private Self self; 25 | 26 | @JsonProperty("eula_acceptance") 27 | private EulaAcceptance eulaAcceptance; 28 | 29 | @JsonProperty("product_files") 30 | private ProductFiles productFiles; 31 | 32 | @JsonProperty("file_groups") 33 | private FileGroups fileGroups; 34 | 35 | @JsonProperty("user_groups") 36 | private UserGroups userGroups; 37 | 38 | @JsonProperty("artifact_references") 39 | private ArtifactReferences artifactReferences; 40 | 41 | public ReleaseLinks( 42 | @JsonProperty("self") Self self, 43 | @JsonProperty("eula_acceptance") EulaAcceptance eulaAcceptance, 44 | @JsonProperty("product_files") ProductFiles productFiles, 45 | @JsonProperty("file_groups") FileGroups fileGroups, 46 | @JsonProperty("user_groups") UserGroups userGroups, 47 | @JsonProperty("artifact_references") ArtifactReferences artifactReferences 48 | ) { 49 | this.self = self; 50 | this.eulaAcceptance = eulaAcceptance; 51 | this.productFiles = productFiles; 52 | this.fileGroups = fileGroups; 53 | this.userGroups = userGroups; 54 | this.artifactReferences = artifactReferences; 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/service/R2dbcAppRelationshipService.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.service; 2 | 3 | import org.cftoolsuite.cfapp.domain.AppRelationship; 4 | import org.cftoolsuite.cfapp.repository.R2dbcAppRelationshipRepository; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.stereotype.Service; 7 | import org.springframework.transaction.annotation.Transactional; 8 | 9 | import lombok.extern.slf4j.Slf4j; 10 | import reactor.core.publisher.Flux; 11 | import reactor.core.publisher.Mono; 12 | 13 | @Slf4j 14 | @Service 15 | public class R2dbcAppRelationshipService implements AppRelationshipService { 16 | 17 | private R2dbcAppRelationshipRepository repo; 18 | 19 | @Autowired 20 | public R2dbcAppRelationshipService(R2dbcAppRelationshipRepository repo) { 21 | this.repo = repo; 22 | } 23 | 24 | @Override 25 | @Transactional 26 | public Mono deleteAll() { 27 | return repo.deleteAll(); 28 | } 29 | 30 | @Override 31 | public Flux findAll() { 32 | return repo.findAll(); 33 | } 34 | 35 | @Override 36 | public Flux findByApplicationId(String applicationId) { 37 | return repo.findByApplicationId(applicationId); 38 | } 39 | 40 | @Override 41 | public Flux findByServiceInstanceId(String serviceInstanceId) { 42 | return repo.findByServiceInstanceId(serviceInstanceId); 43 | } 44 | 45 | @Override 46 | @Transactional 47 | public Mono save(AppRelationship entity) { 48 | return repo 49 | .save(entity) 50 | .onErrorContinue( 51 | (ex, data) -> log.error(String.format("Problem saving application relationship %s.", entity), ex)); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/org/cftoolsuite/cfapp/domain/ResourceType.java: -------------------------------------------------------------------------------- 1 | package org.cftoolsuite.cfapp.domain; 2 | 3 | import java.util.Arrays; 4 | import java.util.List; 5 | import java.util.stream.Collectors; 6 | 7 | import org.springframework.util.Assert; 8 | 9 | import com.fasterxml.jackson.annotation.JsonValue; 10 | 11 | // @see https://v3-apidocs.cloudfoundry.org/version/3.82.0/index.html#resources 12 | // A subset of the available resources accessed via cf v3 api 13 | public enum ResourceType { 14 | 15 | APPS("apps"), 16 | BUILDS("builds"), 17 | BUILDPACKS("buildpacks"), 18 | DEPLOYMENTS("deployments"), 19 | DOMAINS("domains"), 20 | DROPLETS("droplets"), 21 | ISOLATION_SEGMENTS("isolation_segments"), 22 | ORGS("organizations"), 23 | PACKAGES("packages"), 24 | PROCESSES("processes"), 25 | SPACES("spaces"), 26 | STACKS("stacks"), 27 | TASKS("tasks"), 28 | 29 | // experimental resources 30 | REVISIONS("revisions"), 31 | SECURITY_GROUPS("security_groups"), 32 | SERVICE_BINDINGS("service_bindings"), 33 | SERVICE_BROKERS("service_brokers"), 34 | SERVICE_OFFERINGS("service_offerings"), 35 | SERVICE_PLANS("service_plans"); 36 | 37 | 38 | public static ResourceType from(String id) { 39 | ResourceType result = null; 40 | List candidates = Arrays.asList(ResourceType.values()).stream().filter(et -> et.getId().equalsIgnoreCase(id)).collect(Collectors.toList()); 41 | if (candidates != null && candidates.size() == 1) { 42 | result = candidates.get(0); 43 | } 44 | Assert.isTrue(result != null, "Not a valid resource type identifier"); 45 | return result; 46 | } 47 | 48 | private String id; 49 | 50 | ResourceType(String id) { 51 | this.id = id; 52 | } 53 | 54 | @JsonValue 55 | public String getId() { 56 | return id; 57 | } 58 | } 59 | --------------------------------------------------------------------------------